Writing a dumpling chef

The mission of a dumpling chef is to receive network packets from nd-sniff, process those packets, and create dumplings.

To create a dumpling chef you subclass DumplingChef and implement one or both of packet_handler() and interval_handler(). The packet handler is called every time a packet is sniffed by nd-sniff, and the interval handler is called at regular intervals regardless of network packet activity. Whatever the handlers return is automatically packaged into a dumpling by nd-sniff and sent to nd-hub which then sends it on to all the eaters.

Dumpling chefs can be housed in three places:

  1. Python modules accessible via PYTHONPATH (e.g. module.with.chefs)

  2. Python modules located under the directory where nd-sniff is run from

  3. Standalone Python files (e.g. /path/to/chefs.py)

Example chef

The following dumpling chef creates a dumpling for every DNS lookup.

import time
import netdumplings

class DNSLookupChef(netdumplings.DumplingChef):
    def packet_handler(self, packet):
        # The incoming packet is a scapy packet object.
        # https://scapy.readthedocs.io

        # Ignore packets that we don't care about.
        if not packet.haslayer('DNS'):
            return

        # Determine the name of the host that was looked up.
        dns_query = packet.getlayer('DNS')
        query = dns_query.fields['qd']
        hostname = query.qname.decode('utf-8')

        # Generate a dumpling payload from the DNS lookup.
        dumpling_payload = {
            'lookup': {
                'hostname': hostname,
                'when': time.time(),
            }
        }

        # The handler is returning a dict, which will be automatically
        # converted into a dumpling and sent to nd-hub, which will then
        # forward it on to all the eaters.
        return dumpling_payload

If you put the above chef code into a file in your home directory called my_chefs.py then you can tell nd-sniff where to find it with:

$ nd-sniff --chef-module ~/my_chefs.py

Important

The very first thing the above chef does in its packet_handler() method is check the incoming network packet to ensure it’s a packet it actually cares about (in this case a DNS packet):

if not packet.haslayer('DNS'):
    return

Every chef gets every packet sniffed by nd-sniff, and chefs aren’t in control of the sniffer filter being used by nd-sniff, so it’s a good practice to check the packet right at the start to make sure it’s one that the chef wants to process.

Packet and interval handlers

You don’t have to return a dumpling payload for every packet your chef receives. For example you may want your chef to receive and process multiple packets before deciding it’s ready to send a dumpling. You can instead have your chef do something at regular time intervals by implementing an interval_handler() method in your dumpling chef.

Dumplings will only be sent as the result of a packet or interval handler being called if that handler returns something. If a handler doesn’t return anything, or returns None, then no dumpling will be produced. This means your chef can process every packet with its packet handler, but only send dumplings with its interval handler.

The packet format

The packets passed to your packet handler are scapy packets.

If you’re writing your own dumpling chefs then you’re probably going to want to get comfortable with what scapy packets look like. You can do that by using scapy to sniff some packets and interrogate the results in an interactive Python session. Following is one way to get started with that. Since we’re sniffing packets, you may need to run this as an administrator on your system.

$ python
>>> from scapy.all import sniff
>>> packet = sniff(filter='tcp', count=1)
>>> packet[0].show()

The filter argument passed to sniff() is the same PCAP filter format that you’re passing to nd-sniff with the --filter flag. You can read more about the format of the filter string here.

There’s a series of articles called Building Network Tools with Scapy which provides a lot of useful information, including part 4: Looking at Packets.

Maintaining state in a chef

Some chefs will need to maintain state between invocations of its packet and interval handlers. You can achieve this by defining an __init__ method where you initialize the state which can then be accessed by the handler methods. For example:

import time

class StateExampleChef(DumplingChef):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Initialize state.
        self.my_state = []

    def packet_handler(self, packet):
        # Change the state.
        self.my_state.append(time.time())

Telling nd-sniff where to find your chefs

By default nd-sniff will only look for chefs in the netdumplings.dumplingchefs module that comes with netdumplings.

You can override the default behaviour by telling nd-sniff where to find your chefs with the --chef-module flag. You can give this flag either a Python module name or a path to a standalone Python file. You can specify the flag multiple times. For example, the following tells nd-sniff to look for chefs in the mychefs module and in a file called ~/chefs/mychefs.py:

$ nd-sniff --chef-module mychefs --chef-module ~/chefs/mychefs.py

When specifying a Python module name, the module must be importable. The easiest way to do this is to put the chefs in a .py file named the same as the module name and placed in the same directory as where nd-sniff is being run from.

Listing found chefs

You can ask nd-sniff to list all the chefs it can find in the given chef modules by specifying the --chef-list flag:

$ nd-sniff --chef-list \
    --chef-module mychefs \
    --chef-module ~/chefs/mychefs.py \
    --chef-module netdumplings.dumplingchefs

mychefs
  MyDNSChef

~/chefs/mychefs.py
  MyOtherChef
  YetAnotherChef

netdumplings.dumplingchefs
  ARPChef
  DNSLookupChef
  PacketCountChef

More example chefs

netumplings comes with some example chefs in the netdumplings.dumplingchefs module. You can see their source code here: