Wednesday, March 14, 2012

Pyswitch bugfix, and DoS vulnerability in open vSwitch

Pyswitch
I had a bit of time to work on Pyswitch today, and I've cut it back so that it only sets the destination MAC and out port, and that was enough for it to start setting flows properly. You can look at the source if you like, or just focus on the part I've changed:

The function I've modified is forward_l2_packet - as the name suggests, it either floods all ports with the packet it has received, or sends the packet out the correct port and installs a flow in the switch. Here is the function:


def forward_l2_packet(dpid, inport, packet, buf, bufid):    
    dstaddr = packet.dst.tostring()
    if not ord(dstaddr[0]) & 1 and inst.st[dpid].has_key(dstaddr):
        prt = inst.st[dpid][dstaddr]
        if  prt[0] == inport:
            log.err('**warning** learned port = inport', system="pyswitch")
            inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)
        else:
            # We know the outport, set up a flow
            log.msg('installing flow for ' + str(packet), system="pyswitch")
            flow = extract_flow(packet)
            flow[core.IN_PORT] = inport
            actions = [[openflow.OFPAT_OUTPUT, [0, prt[0]]]]
            inst.install_datapath_flow(dpid, flow, CACHE_TIMEOUT, 
                                       openflow.OFP_FLOW_PERMANENT, actions,
                                       bufid, openflow.OFP_DEFAULT_PRIORITY,
                                       inport, buf)
    else:    
        # haven't learned destination MAC. Flood 
        inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)

The key to creating t flow is the extract_flow function from util.py


def extract_flow(ethernet):
    """
    Extracts and returns flow attributes from the given 'ethernet' packet.
    The caller is responsible for setting IN_PORT itself.
    """
    attrs = {}
    attrs[core.DL_SRC] = ethernet.src
    attrs[core.DL_DST] = ethernet.dst
    attrs[core.DL_TYPE] = ethernet.type
    p = ethernet.next


    if isinstance(p, vlan):
        attrs[core.DL_VLAN] = p.id
        attrs[core.DL_VLAN_PCP] = p.pcp
        p = p.next
    else:
        attrs[core.DL_VLAN] = 0xffff # XXX should be written OFP_VLAN_NONE
        attrs[core.DL_VLAN_PCP] = 0


    if isinstance(p, ipv4):
        attrs[core.NW_SRC] = p.srcip
        attrs[core.NW_DST] = p.dstip
        attrs[core.NW_PROTO] = p.protocol
        p = p.next


        if isinstance(p, udp) or isinstance(p, tcp):
            attrs[core.TP_SRC] = p.srcport
            attrs[core.TP_DST] = p.dstport
        else:
            if isinstance(p, icmp):
                attrs[core.TP_SRC] = p.type
                attrs[core.TP_DST] = p.code
            else:
                attrs[core.TP_SRC] = 0
                attrs[core.TP_DST] = 0
    else:
        attrs[core.NW_SRC] = 0
        attrs[core.NW_DST] = 0
        attrs[core.NW_PROTO] = 0
        attrs[core.TP_SRC] = 0
        attrs[core.TP_DST] = 0
    return attrs

Now, if we're just making a basic switch, this does way more than we need - why would a switch care about layer 4 protocols? Fortunately, open vSwitch on the Pronto ignores most of it because it uses DL_TYPE=0x8100 (which means the packet is 802.1q VLAN tagged, and the actual ethertype is 4 bytes futher up), but having the wrong DL_TYPE is why nothing ends up matching the flow...

Util.py needs to be fixed to interpret VLANs properly, but in the meantime, pyswitch will work fine as a simple layer two switch if we use a cut-down version of the extract_flow function. And here it is:

def create_l2_out_flow(ethernet):
    attrs = {}
    attrs[core.DL_DST] = ethernet.dst
    return attrs

Simple, right? Now we use this instead of extract_flow, and then we can walk through what the function does in detail:

ddef forward_l2_packet(dpid, inport, packet, buf, bufid):    
    dstaddr = packet.dst.tostring()
    if not ord(dstaddr[0]) & 1 and inst.st[dpid].has_key(dstaddr):
[...]

    else:  
        # haven't learned destination MAC. Flood
        inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)


This pulls the destination MAC address out of the packet, converts it to a string, and makes sure the first character is 0 = unicast. If this is the case, it checks to see if it's learnt it before, and if so, then we can proceed. Otherwise, it floods to all ports - correct for both broadcast/multicast and unknown MAC addresses.

        prt = inst.st[dpid][dstaddr]
        if  prt[0] == inport:
            log.err('**warning** learned port = inport', system="pyswitch")
            inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)

If the destination MAC is assigned to the source port then something is weird (either a spoof or a loop in the network), so behave like a hub for this packet

        else:
            # We know the outport, set up a flow
            log.msg('installing flow for ' + str(packet), system="pyswitch")
            # sam edit - just load dest address, the rest doesn't matter
            flow = create_l2_out_flow(packet)
            actions = [[openflow.OFPAT_OUTPUT, [0, prt[0]]]]
            inst.install_datapath_flow(dpid, flow, CACHE_TIMEOUT,
                                       openflow.OFP_FLOW_PERMANENT, actions,
                                       bufid, openflow.OFP_DEFAULT_PRIORITY,
                                       inport, buf)

This is the switch part - we create our very specific flow with our new function (just destination MAC address - not all 10 or so parts to match on), set the action to output to the correct port, then call install_datapath_flow (part of nox::lib::core::Component), which sends back the new flow and instruction on where to send the packet. All done, and works well, except for one thing:

Open vSwitch DoS (probably one of many)
The problem with OpenFlow that everybody points out is that you can only really send 10 packets per second to your controller. You can try and optimise this if you want, but this switch-controller connection is where the battle will be fought to make OpenFlow perform better. I didn't think this would be a problem with the Pronto, because I assumed that open vSwitch would process packets somewhat like this:


  1. Find flow for packet - if found, follow the actions and go to next packet
  2. Send packet to controller
  3. Get packet and flow back from controller, follow instruction for this packet and install flow
  4. Go back to 1 for next packet.
Unfortunately, it appears that open vSwitch does things a little differently:

  1. Find flow for packet - if found, follow the actions and go to next packet
  2. Send packet to controller
  3. Get packet and flow back from controller, follow instruction for this packet and add flow to some queue somewhere
  4. Go back to 1 for next packet
  5. If no more packets waiting, look at the queue and install the flow
Surprisingly enough, this works fine for TCP - the 3-way handshake gives the switch enough downtime to install the flow, and get ready for the influx of data. However, if you surprise it with 500Mb/s of UDP iperf, you find the receiving server only getting ~150Kb/s, every single packet going to the controller, and no flow being installed!

Fortunately, the staff at Pronto have been awesome to work with, so I'm hoping we'll get a solution soon, and in the meantime, I'll try to find a workaround myself. If you're testing and stuck in a similar situation, either start off with a little UDP test first, or even ping the other host before starting your iperf - this will set the flows, and then you can send as much data as you like!

No comments:

Post a Comment