Can't Parse DHCP packets with Ryu's get_protocol(dhcp.dhcp)

776 views Asked by At

I'm using the Ryu SDN controller with an Open vSwitch on mininet using OpenFlow 1.3 to parse DHCP packets. Following online examples and the Ryu resources, I've implemented a DHCP packet parser. However, it does not work as I expected it to, and I'm wondering if anyone has any insight as to why my first solution does not work?

An example of a code snippet for parsing a DHCP packet is below:

from ryu.lib.packet import dhcp
...
...
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
    msg = ev.msg
    datapath = msg.datapath
    pkt = packet.Packet(msg.data)
    dhcpPacket = pkt.get_protocol(dhcp.dhcp)

My code follows a similar vein:

from ryu.lib.packet import dhcp
...
...
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
    pkt = {}
    pkt['msg'] = ev.msg
    pkt['dp'] = pkt['msg'].datapath
    pkt['pkt'] = packet.Packet(pkt['msg'].data)
    pkt['dhcp'] = pkt['pkt'].get_protocol(dhcp.dhcp)

This seems reasonable as I am following this exact sequence with other protocols like ARP, ICMP, IP, etc. Examples below.

pkt['arp'] = pkt['pkt'].get_protocol(arp.arp)
pkt['ip'] = pkt['pkt'].get_protocol(ipv4.ipv4)
pkt['icmp'] = pkt['pkt'].get_protocol(icmp.icmp)

The only problem is that the three parsers I list above actually return data, while the get_protocol for DHCP consistently returns None. I have already tested this by sending DHCP packets through my switch.

What does work is the following code snippet where I identify packet lists having more than three values. I save the value at index three and set that as my DHCP packet. In the DHCP packet, I concentrate of parsing the string at index 2. That contains the data I'm interested in.

# pkt['dhcp'] = pkt['pkt'].get_protocol(dhcp.dhcp)
# Check if pkt['pkt]] > 3 elements, if so, parse DHCP string
#Standard pkt['dhcp'] = (None, None, String)
if len(pkt['pkt']) > 3:
    pkt['dhcp'] = dhcp.dhcp.parser(pkt['pkt'][3])
    pkt['op'] = hex(ord(dhcp_p[2][0]))
    pkt['htype'] = hex(ord(dhcp_p[2][1]))
    pkt['hlen'] = hex(ord(dhcp_p[2][2]))
    pkt['hops'] = hex(ord(dhcp_p[2][3]))

    def parseDHCP(pkt_d,start,stop):
        s_value = ''
        stop += 1
        for val in range(start,stop):
        s_value += str(hex(ord(pkt_d[val])))
        return s_value

    pkt['xid'] = parseDHCP(dhcp_p[2],4,7)
    pkt['secs'] = parseDHCP(dhcp_p[2],8,9)
    pkt['flags'] = parseDHCP(dhcp_p[2],10,11)
    pkt['ciaddr'] = parseDHCP(dhcp_p[2],12,15)
    pkt['yiaddr'] = parseDHCP(dhcp_p[2],16,19)
    pkt['siaddr'] = parseDHCP(dhcp_p[2],20,23)
    pkt['giaddr'] = parseDHCP(dhcp_p[2],24,27)
    pkt['chaddr'] = parseDHCP(dhcp_p[2],28,33)
    pkt['pad'] = parseDHCP(dhcp_p[2],34,43)

A print out of these values looks like so:

0x1
0x1
0x6
0x0
0x440x30x980x11
0x00x0
0x00x0
0x00x00x00x0
0x00x00x00x0
0x00x00x00x0
0x00x00x00x0
0x7e0x1d0xcc0xe70xee0x4f
0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x0

The above code allows me to observe the contents of DHCP packets, but I'm really trying to figure out why I'm not achieving similar results using the pkt['pkt'].get_protocol(dhcp.dhcp) method?

1

There are 1 answers

0
Jacob On BEST ANSWER

Ok, I found the problem. In lines 200 – 218 of dhcp.py there is a try except statement. I pulled the cls._parser out of the try to see the error it throws and I get this:

Ryuretic_coupler: Exception occurred during handler processing. Backtrace    
from offending handler [initial_event] servicing event [EventOFPPacketIn] follows.
Traceback (most recent call last):
  File "/home/ubuntu/ryu/ryu/base/app_manager.py", line 290, in _event_loop
    handler(ev)
  File "/home/ubuntu/ryu/ryu/app/Ryuretic/Ryuretic.py", line 72, in initial_event
    pkt = parsPkt.handle_pkt(ev)
  File "/home/ubuntu/ryu/ryu/app/Ryuretic/Pkt_Parse13.py", line 81, in handle_pkt
    dhcp_p = pkt['dhcp'] = dhcp.dhcp.parser(pkt['pkt'][3])
  File "/home/ubuntu/ryu/ryu/lib/packet/dhcp.py", line 212, in parser
    return cls._parser(buf)
  File "/home/ubuntu/ryu/ryu/lib/packet/dhcp.py", line 192, in _parser
    ) = struct.unpack_from(unpack_str, buf)
error: unpack_from requires a buffer of at least 233 bytes

So, dhcp.py is not receiving the 233 bytes that it requires. Unfortunately, I am using an Open vSwitch on the VM provided by SDN hub, and 128 bytes seems to be a limit. So, I have a conflict with Ryu's dhcp.py file. My solution was to modify dhcp.py. How that was done follows.

Before modifying the code, I recommend you first update your Ryu controller. The procedures are as follows:

Step 1: If you are using a VM. Take a stapshot or clone it now.

Step 2: Update Ryu

cd ryu
git pull

If you were running an older version like I was, then the following error may occur when you next attempt to run your Ryu controller:

Traceback (most recent call last):
File "./bin/ryu-manager", line 18, in <module>
  from ryu.cmd.manager import main
File "/home/ubuntu/ryu/ryu/cmd/manager.py", line 31, in <module>
  from ryu.base.app_manager import AppManager
File "/home/ubuntu/ryu/ryu/base/app_manager.py", line 37, in <module>
  from ryu.controller.controller import Datapath
File "/home/ubuntu/ryu/ryu/controller/controller.py", line 74, in <module>
  help='Maximum number of unreplied echo requests before datapath is disconnected.')
File "/usr/local/lib/python2.7/dist-packages/oslo_config/cfg.py", line 1033, in __init__
  super(IntOpt, self).__init__(name, type=types.Integer(), **kwargs)
TypeError: __init__() got an unexpected keyword argument 'min'

Step 3: Update your oslo_config file

sudo pip install oslo.config --upgrade

The TypeError from step 2 should now be resolved. (Hopefully, you cloned your VM just in case.)

Step 3: Modify Ryu's dhcp.py file

Ryu's dhcp.py file (located at /ryu/ryu/lib/packet) expects to receive a buffer of more than 235 bytes. Otherwise, it throws and error and returns nothing to the controller. Since my buffer only receives a buffer size of about 81 bytes. I modified the Ryu dhcp.py file as follows.

The error occurs because dhcp.py specifies a string format of '!BBBBIHH4s4s4s4s16s64s128s'. In #1, I created a second option. Do so, allows me to insert a few if statements to handle the packet differently if a packet arrives that is smaller than 100 bytes. Likewise, if the packet is greater than 235 bytes, then dhcp.py's handles the packet normally and returns the extra values.

class dhcp(packet_base.PacketBase):
    """DHCP (RFC 2131) header encoder/decoder class.
                ....deleted....
    """
    _MIN_LEN = 236
    _HLEN_UNPACK_STR = '!BBB'
    _HLEN_UNPACK_LEN = struct.calcsize(_HLEN_UNPACK_STR)
    _DHCP_UNPACK_STR = '!BIHH4s4s4s4s%ds%ds64s128s'
    ##################################################
    #1(mod) Created second option for unpacking string
    _DHCP_UNPACK_STR2 = '!BIHH4s4s4s4s%ds%ds40s'
    _DHCP_PACK_STR = '!BBBBIHH4s4s4s4s16s64s128s'
    #################################################
    _DHCP_CHADDR_LEN = 16
    _HARDWARE_TYPE_ETHERNET = 1
    _class_prefixes = ['options']
    _TYPE = {
        'ascii': [
            'ciaddr', 'yiaddr', 'siaddr', 'giaddr', 'chaddr', 'sname'
        ]
    }

    def __init__(self, op, chaddr, options, htype=_HARDWARE_TYPE_ETHERNET,
                 hlen=0, hops=0, xid=None, secs=0, flags=0,
                 ciaddr='0.0.0.0', yiaddr='0.0.0.0', siaddr='0.0.0.0',
                 giaddr='0.0.0.0', sname='', boot_file=b''):
        super(dhcp, self).__init__()
        #...Deleted No Changes made to init. 

    @classmethod
    def _parser(cls, buf):
        (op, htype, hlen) = struct.unpack_from(cls._HLEN_UNPACK_STR, buf)
        buf = buf[cls._HLEN_UNPACK_LEN:]
        ####################################################
        #2(mod) provided option for smaller packet sizes
        if len(buf) < 100:
            unpack_str = cls._DHCP_UNPACK_STR2 % (hlen,
                                             (cls._DHCP_CHADDR_LEN - hlen))
        else:
            unpack_str = cls._DHCP_UNPACK_STR % (hlen,
                                             (cls._DHCP_CHADDR_LEN - hlen))            
        #####################################################
        min_len = struct.calcsize(unpack_str)
        ######################################################
        #3(mod) provided option for smaller packets, set bootfile to b''
        if min_len > 233:
            (hops, xid, secs, flags, ciaddr, yiaddr, siaddr, giaddr,  
             chaddr, dummy, sname, boot_file
             ) = struct.unpack_from(unpack_str, buf)
        else:
            boot_file=b''
            (hops, xid, secs, flags, ciaddr, yiaddr, siaddr, giaddr,  
             chaddr, dummy, sname, boot_file
             ) = struct.unpack_from(unpack_str, buf)+(boot_file,)
        ########################################################    
        length = min_len
        ########################################################
        # (mod) provided option for smaller packet sizes, no parse_opt
        if len(buf) > 233: 
            parse_opt = options.parser(buf[min_len:])
            length += parse_opt.options_len
        else:
            parse_opt = None
            length = min_len
        #########################################################   
        return (cls(op, addrconv.mac.bin_to_text(chaddr), parse_opt,
                    htype, hlen, hops, xid, secs, flags,
                    addrconv.ipv4.bin_to_text(ciaddr),
                    addrconv.ipv4.bin_to_text(yiaddr),
                    addrconv.ipv4.bin_to_text(siaddr),
                    addrconv.ipv4.bin_to_text(giaddr),
                    sname.decode('ascii'), boot_file),
                None, buf[length:])

The complete file will soon be on https://github.com/Ryuretic/RyureticLabs/tree/master/ryu/ryu/app/Ryuretic/Support_Files.

If you make these adjustements to the dhcp.py file, then you will gain access to the following header fields:

============== ====================
Attribute      Description
============== ====================
op             Message op code / message type.\
               1 = BOOTREQUEST, 2 = BOOTREPLY
htype          Hardware address type (e.g.  '1' = 10mb ethernet).
hlen           Hardware address length (e.g.  '6' = 10mb ethernet).
hops           Client sets to zero, optionally used by relay agent\
               when booting via a relay agent.
xid            Transaction ID, a random number chosen by the client,\
               used by the client and serverto associate messages\
               and responses between a client and a server.
secs           Filled in by client, seconds elapsed since client\
               began address acquisition or renewal process.
flags          Flags.
ciaddr         Client IP address; only filled in if client is in\
               BOUND, RENEW or REBINDING state and can respond\
               to ARP requests.
yiaddr         'your' (client) IP address.
siaddr         IP address of next server to use in bootstrap;\
               returned in DHCPOFFER, DHserver.
giaddr         Relay agent IP address, used in booting via a\
               relay agent.
chaddr         Client hardware address.
sname(partial) Optional server host name, null terminated string.