Wednesday, November 23, 2011

DCI Sample Implementation

Download: https://github.com/dimitrs/DCI-NIDS/tree/DCI-NIDS-1

In this post I present an experimental network protocol analyzer implementation (in C++) based on the Data, context and interaction (DCI) paradigm and code snippets from Snort. My intention was to get first hand experience with DCI in C++, understand its benefits and its limitations, and evaluate its applicability to full-scale projects. Since I was unsuccessful in finding DCI C++ examples anywhere, my code represents the understanding of DCI that I have gained from piecing together the various code fragments from documentation. I admit that I find it a bit strange that there are no examples (that I could find). Is there an impediment to using DCI in C++ ? I did come across a couple of issues in my implementation. But first, let me list the goals of DCI as described by the DCI wiki entry and to state that to some degree or another I did experience the benefit of these goals:


  • To improve the readability of object-oriented code by giving system behavior first-class status;
  • To cleanly separate code for rapidly changing system behavior (what the system does) from code for slowly changing domain knowledge (what the system is), instead of combining both in one class interface;
  • To help software developers reason about system-level state and behavior instead of only object state and behavior;
  • To support an object style of thinking that is close to peoples' mental models, rather than the class style of thinking that overshadowed object thinking early in the history of object-oriented programming languages.


DCI consists of three parts: Data, context and interaction. Each of which I list below together with some observations about each one concerning the implementation I present in this post. One thing I should mention first is that I took most of the underlying code from Snort. My goal is not to re-design Snort. After all, it only took me about half and hour to find and understand the TCP/IP decoding and processing parts of the code, which could be a sign that it is fairly well designed even though its implemented in C. What’s more, I have re-implemented a very small part of Snort – just enough to take DCI out for a test-run. Take a quick look at the source code before reading on.

Data

The domain objects. They contain very little interaction code and mainly consist of getter and setter methods. I have used the prefix “Do” to denote data classes e.g. DoIPv4Packet, DoIPv6Packet and DoTCPpacket. They represent the IPv4, IPv6, TCP parts of the packet. You will notice that DoIPv4Packet and DoIPv6Packet are practically identical (see “Interaction”).


Context

Implements the use cases of the system. A context includes the roles and data objects and knows how to bind them. The context also provides a mechanism by which any role executing within that context can refer to the other roles in that context. I have used the prefix “Context” to denote context classes e.g. ContextIP. The ContextIP context decodes IP packet headers and applies IP layer rules and includes the specific roles and data objects specific to those tasks. A single data object may simultaneously play several roles. In the ContextIP context, a DoIPv4Packet object can play the Role_IPv4decoder and Role_IPrules roles (see “Interaction”).

The context knows which roles and data objects are to be used within its scope. Data objects are retrieved or created by the context. Below, is the ContextIP constructor. It creates the relevant data object and assigns the roles they are to play.


ContextIP::ContextIP(const void* packet) :
    decode_(NULL), rules_(NULL), pkt_(packet)
{
    const iphdr* hdr = static_cast<const iphdr*>(packet);   
    if (hdr->version == 4)
    {
        DoIPv4Packet* obj = new DoIPv4Packet;        
        decode_ = obj;
        rules_ = obj;
    }
    else if (hdr->version == 6)
    {
        DoIPv6Packet* obj = new DoIPv6Packet;        
        decode_ = obj;
        rules_ = obj;
    }
}


In one context I had a difficult time implementing the creation/retrieval of the data objects. The type of data object to be created/retrieved depends on what came before i.e. the state of previous data objects. Is it permissible to use a role from within a context constructor ? In the end I ended up in the strange situation where a role has no SELF because it has not been created yet, but uses other roles (see ContextStream). ContextStream creates or retrieves a TCP/UDP stream (or 5-tuple flow) and processes it. I am sure that this part needs to be reworked.


Interaction 


The Interaction is "what the system does." and is implemented as roles objects play at run time. I have used the prefix “Role” to denote role classes e.g. Role_IPv4decoder, Role_IPv6decoder, Role_TCPdecoderImpl.
Role objects combine the state and methods of a Data object with methods (but no state, as Roles are stateless) from one or more Roles. Role methods are injected into Data objects by use of traits. Unfortunately, the injection can not be done at run-time. The IP packet data object could play either the Role_IPv4decoder role or the Role_IPv6decoder role depending on the IP version. However, since both roles can not be injected onto the same data object, I had to duplicate data object itself – hence DoIPv4Packet and DoIPv6Packet are practically identical.
According to DCI documentation, a Role should only address another object in terms of its methodless Role i.e. a pure virtual base class. I found that it was essential to do this if only to compile successfully because of the dependencies between the Role, Data and Context classes. That is the reason for Role_TCPdecoder and Role_TCPdecoderImpl. Even though there may never be a different kind of TCP decoder I had to give the Role_TCPdecoderImpl a pure base class.
Below, you can see an IPv4 decoder role method. The role uses SELF which binds to the object playing the current Role. Code within the method invokes methods of the Data part of the object using SELF. Methods of other roles can also be invoked. RULES binds to the Role_IPrules role and allows one role to invoke methods on another role. A single data object may simultaneously play several roles. The DoIPv4Packet object play the Role_IPv4decoder and Role_IPrules roles.

template <class ConcreteDerived>

void Role_IPv4decoder<ConcreteDerived>::accept(const void* packet)
{
    const iphdr* ip = static_cast<const iphdr*>(packet);
   
    SELF->data(packet);
   
    SELF->srcip()->setAddr(ip->saddr);
    SELF->dstip()->setAddr(ip->daddr);
       
    if (ip->protocol==6)
    {
        SELF->proto(TCP_PROTO);
    }
    else if (ip->protocol==17)
    {
        SELF->proto(UDP_PROTO);       
    }
    else {
        SELF->proto(UNKNOWN_PROTO);               
    }
   
    SELF->totallength(ntohs(ip->tot_len));
    SELF->headerLength(ip->ihl*4);
    SELF->id(ntohs(ip->id));
    SELF->frag_flag(ntohs(ip->frag_off) & 0x3fff);
    SELF->frag_offset(ntohs(ip->frag_off));
   
    // Apply IP header rules
    RULES->match();
                   
    if (Role_IPv4decoder<ConcreteDerived>::getProto() == TCP_PROTO)
    {
        ContextTCP context(SELF, packet);
        context.doit();      
    }
}