Live Packet Sniffing With an Embedded Module

live stream

SniPCAP is improved and now streaming!

As it was mentioned in the first article, SniPCAP had a lot of room to improve on. The following lists the basic functionalities of SniPCAP:

  • audit information
  • monitor bandwidth usage
  • diagnose problems or bottlenecks in the network
  • discover devices on the network
  • detect network intrusion through packet analysis
  • convert network traffic into a user readable format
  • log traffic

The previous version of SniPCAP was only a minimum product that temporarily captured a small amount of data; increased scalability and better usability was a necessity (more details on the first part of this project could be found here). This article shows the development of a more scalable product on the NetBurner Module.

Scalability

When working with an embedded system, the biggest challenge is always the management of limited resources. Previously, the project hit its limitation at 50 MB of temporary storage, allowing only limited capture duration. The first version of SniPCAP was simply not viable in a real network environment. In order to make it a useful application, it had to be rebuilt.

One way to scale an application on the MOD54417 is to utilize the onboard micro-SD card slot. I will go into more detail on how to utilize this on your program later. Although the micro-SD card provides more space, it is a secondary storage; if we were to stream each packet as we receive it, a huge overhead will occur as a result of file system operations. To avoid this overhead, we utilize RAM(Random-access Memory) to temporarily store incoming packets while a separate task (or thread) writes to the SD card making the memory reusable. In order for this to work, a new data structure must be implemented.

Circular Buffer

The way that the data should be processed need not be shuffled on consumption and is always FIFO (First In First Out). Therefore, the circular buffer structure is the best implementation. However, there are a few details we need to be worry about when implementing a circular buffer. For the sake of explanation, I will use these terms to describe them:

  • producer: a pointer to the location where the packets are being written in the memory.
  • consumer: a pointer to the location where packets being read and consumed from the memory.

First, lets begin with the case that nothing has been written to the buffer. How does the consumer know when there is a valid packet to process?
full empty buffer

Since the producer and consumer are separate tasks running simultaneously, we use semaphores to synchronize two tasks (semaphore and synchronization between tasks will be covered in the next section). This method makes the consumer wait to read until the producer receives the packet and stores it into the capture buffer.

What happens at the end of the buffer? When the producer or consumer recognizes that they are close to the end of the buffer, it points back to the beginning of the buffer (hence the name, circular buffer).

Lastly, the semaphore lock is in place, so we don’t have to worry about the consumer consuming too fast. However, if the producer produces too fast, it could override and flood the buffer. To prevent the overflow, you would put in check like this:

// check fail condition - head eats tail
// bufferIndex is Producer/Start
// capBuffIndex is Consumer/End
// case 1.
if ((capBuffIndex > bufferIndex)
        && (capBuffIndex - bufferIndex) < BUFF_PADDING) {
    PauseCapture();
}
// case 2.
if ((bufferIndex > capBuffIndex)
        && ((END - bufferIndex) + (capBuffIndex - START)) < BUFF_PADDING) {
    PauseCapture();
}

When the producer is behind the consumer (produced enough to write the whole buffer and circle behind the consumer) and runs out of space (have space that is less than the set padding value) it stops capturing incoming packets. The two case presented above do occur with the same condition – the head eats the tail. It’s easier to realize it with the following picture:

buffer
Full/Empty Buffer Distinction

This is the first case where END(Consumer) > START(Producer).

buffer 2
Full/Empty Buffer Distinction

This is the second case where START(Producer) > END(Consumer).

The producer should never wait for the consumer to free up before halting because of the nature of the application; if it waits for the consumer after the buffer is full, we will have packet loss in sniffing and inaccurate logging. There are other ways to implement the circular buffer which are succinctly outlined here. Now that the memory is ready to stream, implementing the file access is the next order of business.

Mounting Drive / Working with SD cards

Implementing access to the file system was easy to integrate after following some examples and studying the well-written library. To access the file system on the module, we just need to call f_enterFS() function once in each priority:

void UserMain(void * pd) {

    init();
    EnablePromiscuous();
    ...
    f_enterFS();
    drv = OpenOnBoardFlash();
    f_chdrive(drv);
    OSSimpleTaskCreate(StreamTask, WRITE_PRIO);
    OSChangePrio( HTTP_PRIO );
    f_enterFS();
    OSChangePrio( FTPD_PRIO );
    f_enterFS();
    OSChangePrio( MAIN_PRIO );

First, init() would UserMain(the main task) at the main priority level. Calling f_enterFS() from the main task establishes a connection from UserMain to the file system. The OpenOnBoardFlash() function then recognizes the mounted file system on the onboard SD card slot. The f_chdrive(drvnumber) is called to change the drive. Once these are initialized, other file system functions could be used, such as f_chdir(dirname) which changes directory. Now that the file system is in place, we can study a bit more abstract concepts that allows our application to run without a scheduling conflict.

Semaphore / Priority Scheduling

As I mentioned earlier, streaming requires a circular buffer to keep reusing limited memory and a secondary storage to store streamed data. I also mentioned that circular buffers require the use of a semaphore. So what is a semaphore? A semaphore is simply a common variable accessed by different tasks to access shared or common resources. There are two ways that the semaphore is used in SniPCAP:

  • As mentioned earlier, the producer and consumer share the common variable to keep track of available packets to be read from the buffer.
  • Wake up the task that streams from the SD card(on start of the capture).

Here is a basic example of how the semaphore is used:

...
#include <ucos.h> // first include UCOS header
...
OS_SEM sem_name;  // declare semaphore sem_name
...
void UserMain (void * pd) {
    ...
    OSSemInit (&sem_name, 0); // this line initiate the sem_name
                              // with the given integer on second param
...
}
...

void Producer () {
    OSSemPost (&sem_name);  // when Producer() is called increment sem_name
}

/*
 * when Consumer() is called it waits until sem_name to be greater
 * than 0, second param 0 indicates the wait time is set to infinite
 * for example if second param is set to 2, then it waits for 2 ticks
 * before returning OS_TIMEOUT else return OS_NO_ERR.
 */
void Consumer () {
    OSSemPend(&sem_name, 0); 
    /* do your thing here */
}

Similarly, to control limited resources (processing power in this case), NetBurner’s unique real-time operating system (RTOS) uses a fixed-priority preemptive scheduling algorithm. The scheduler ensures that the processor executes the highest (in NetBurner’s RTOS, the higher the priority, the lower the number) assigned priority task. This allows the application to write all the packets into the buffer without any loss, and when free, it switches to the consumer to read from the buffer and write to the SD card. NetBurner’s RTOS utilizes system priority set as:
//More detail can be found in the UCOS library documentation uCOSlibrary.pdf
#define MAIN_PRIO (50)
// this is where our priority will go

/* Runtime library driver and support task priorities */
#define HTTP_PRIO                (45)
#define PPP_PRIO                 (44)
...
#define ETHER_SEND_PRIO          (38) // this is the producer task priority
```
The consumer task be higher priority than main, but lower priority than other system tasks and the producer task:
```
#define WRITE_PRIO ( MAIN_PRIO - 2 )
...
void UserMain (void * pd) {
    ...
    OSSimpleTaskCreate(StreamTask, WRITE_PRIO);
    ...
}

Optimization

With the basic functionality in place, we can now think of a few ways to optimize the product. A few of the optimizations made for this project are:

  • Pend data transmission until the message fits the file system block size to reduce overhead
  • Stream data to the SD card while waiting on incoming packets to reduce downtime
  • Directory listing functionality for better usability

If a public bus that operates between two cities run for a person at a time it will be very inefficient and costly. Similarly, if packets are streamed to the file system every time a packet is captured, it will lead to multiple small transfers. Since accessing secondary storage is slower, it will create a large overhead. Note that the file system’s block size is 512 bytes and the maximum transmission unit for Ethernet is 1500 bytes, so we want to fill the message up to the block size as much as possible before transmitting to ensure maximum throughput and minimum overhead. To accomplish this, we must make another buffer in between the capture buffer and file system (this will be denoted as the SD buffer). In the bus analogy, the SD buffer acts like a bus station where enough people are gathered and wait before the bus leaves for the other city.

However, it would become a problem if there are no more people coming to the bus station, leaving people who are already waiting at the station to be stuck there forever. To solve this problem, I utilized the OSPendNoWait(&sem_name) function to see if there are any more pending packets. If there are no more packets to be processed, the we use this free time to write the current content of the SD buffer to the file system.

if (OSSemPendNoWait(&BUF_SEM) == OS_TIMEOUT) {
    writeall (fd, (char*) &sdBuffer, sdBuffIndex);
    fSize += sdBuffIndex;
    sdBuffIndex = 0;
    iprintf("!");
} else {
    OSSemPost(&BUF_SEM);
}

This condition will check if there are no more incoming packets to process, but the semaphore will be adjusted according to the results of the check as we didn’t actually consume a packet.

Another big optimization for the users is to create a directory listing. The new version of SniPCAP allows the user to define names in order to save different file captures. I implemented the FTP directory listing to the user interface so that user can remotely download anything that is on the SD card.

Useful Tools

I have been using NetBurner Eclipse and Sublime 3 to compile and edit while using the multi-threaded TTY serial terminal as the debugging tool. Multi-threaded TTY was exceptionally useful as it also displayed trap reports. If you include and enable smarttrap.h in your code, then you could use a few GCC command line tools to understand trap reports and debug it.

#include <smarttrap.h>
...
void UserMain(void * d) {
    ...
    EnableSmartTraps();
    ...
}

This is the example code to include and enable smart trap.

When the program compile (or breaks), the compiler creates the Executable and Linkable Format (.ELF) file in the Release folder. In the terminal command prompt, navigate to the folder and type:

m68k-elf-addr2line "Project Name.elf" "Faulted Address" -f -e

This GCC tool will dump the display the address of the function specified in “Faulted Address”.

m68k-elf-objdump -s -d "filename.elf" > "dump.txt"

This GCC tool will dump the .elf file into readable format file called dump.txt where you can trace your bug in assembly syntax. You can find more information regarding this topic posted in the NetBurner Community Forum

Closing Note

Overall, it was a great experience working with the NetBurner module and its libraries, documentation, and tools that have been provided for developers to build their own application. I was able to start from scratch to pushing out the minimum product, then adding features to make a useful application. From low level memory management to front end UI to designing a product with use case in mind, I learned and grew as a developer during the project; as I progressed, I even found a bug in the provided example code, fixed it and submitted an official report as well. The bug was only in a sample code, but as an intern, you bet I felt accomplished afterwards. Developing my product and seeing other interns’ projects I realized how powerful and flexible the NetBurner module can be. I hope reading this guide helped you on your way to developing something great! Thank you for reading! If you are curious about the details of SniPCAP version 2, check out my source code.

Share this post

Subscribe to our Newsletter

Get monthly updates from our Learn Blog with the latest in IoT and Embedded technology news, trends, tutorial and best practices. Or just opt in for product change notifications.

Leave a Reply
Click to access the login or register cheese