Merge pull request #24 from cdickmann/badpackets
Add packet parsing/matching verification
diff --git a/src/python/oftest/parse.py b/src/python/oftest/parse.py
index 8826c0c..a3421a1 100644
--- a/src/python/oftest/parse.py
+++ b/src/python/oftest/parse.py
@@ -245,8 +245,10 @@
except:
icmp = None
- # @todo arp is not yet supported
- arp = None
+ try:
+ arp = ether[scapy.ARP]
+ except:
+ arp = None
return (dot1q, ip, tcp, udp, icmp, arp)
def packet_to_flow_match(packet, pkt_format="L2"):
@@ -328,7 +330,14 @@
match.nw_proto = 1
match.tp_src = icmp.type
match.tp_dst = icmp.code
+ match.wildcards &= ~OFPFW_NW_PROTO
- #@todo Implement ARP fields
+ if arp:
+ match.nw_proto = arp.op
+ match.wildcards &= ~OFPFW_NW_PROTO
+ match.nw_src = parse_ip(arp.psrc)
+ match.wildcards &= ~OFPFW_NW_SRC_MASK
+ match.nw_dst = parse_ip(arp.pdst)
+ match.wildcards &= ~OFPFW_NW_DST_MASK
return match
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index 3be9c32..53c18fe 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -64,7 +64,9 @@
ip_dst='192.168.0.2',
ip_tos=0,
tcp_sport=1234,
- tcp_dport=80
+ tcp_dport=80,
+ ip_ihl=None,
+ ip_options=False
):
"""
Return a simple dataplane TCP packet
@@ -94,14 +96,22 @@
if (dl_vlan_enable):
pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
scapy.Dot1Q(prio=dl_vlan_pcp, id=dl_vlan_cfi, vlan=dl_vlan)/ \
- scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=ip_ihl)/ \
scapy.TCP(sport=tcp_sport, dport=tcp_dport)
else:
- pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
- scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos)/ \
- scapy.TCP(sport=tcp_sport, dport=tcp_dport)
+ if not ip_options:
+ pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=ip_ihl)/ \
+ scapy.TCP(sport=tcp_sport, dport=tcp_dport)
+ else:
+ pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=ip_ihl, options=ip_options)/ \
+ scapy.TCP(sport=tcp_sport, dport=tcp_dport)
pkt = pkt/("D" * (pktlen - len(pkt)))
+
+ #print pkt.show()
+ #print scapy.Ether(str(pkt)).show()
return pkt
diff --git a/tests/pktact.py b/tests/pktact.py
index 10dbdb3..d033f2d 100644
--- a/tests/pktact.py
+++ b/tests/pktact.py
@@ -16,6 +16,7 @@
import logging
import time
import unittest
+import random
from oftest import config
import oftest.controller as controller
@@ -27,6 +28,8 @@
import oftest.base_tests as base_tests
import basic # for IterCases
+from oftest.parse import parse_mac, parse_ip
+
from oftest.testutils import *
WILDCARD_VALUES = [ofp.OFPFW_IN_PORT,
@@ -1931,5 +1934,768 @@
testField("tp_src", 0xffff)
testField("tp_dst", 0xffff)
+class DirectBadPacketBase(base_tests.SimpleDataPlane):
+ """
+ Base class for sending single packets with single flow table entries.
+ Used to verify matching of unusual packets and parsing/matching of
+ corrupted packets.
+
+ The idea is to generate packets that may either be totally malformed or
+ malformed just enough to trick the flow matcher into making mistakes.
+
+ Generate a 'bad' packet
+ Generate and install a matching flow
+ Add action to direct the packet to an egress port
+ Send the packet to ingress dataplane port
+ Verify the packet is received at the egress port only
+ """
+
+ RESULT_MATCH = "MATCH"
+ RESULT_NOMATCH = "NO MATCH"
+ RESULT_ANY = "ANY MATCH"
+
+ def runTest(self):
+ pass
+ # TODO:
+ # - ICMP?
+ # - VLAN?
+ # - action
+
+ def pktToStr(self, pkt):
+ from cStringIO import StringIO
+ backup = sys.stdout
+ sys.stdout = StringIO()
+ pkt.show2()
+ out = sys.stdout.getvalue()
+ sys.stdout.close()
+ sys.stdout = backup
+ return out
+
+ def createMatch(self, **kwargs):
+ match = ofp.ofp_match()
+ match.wildcards = ofp.OFPFW_ALL
+ fields = {
+ 'dl_dst': ofp.OFPFW_DL_DST,
+ 'dl_src': ofp.OFPFW_DL_SRC,
+ 'dl_type': ofp.OFPFW_DL_TYPE,
+ 'dl_vlan': ofp.OFPFW_DL_VLAN,
+ 'nw_src': ofp.OFPFW_NW_SRC_MASK,
+ 'nw_dst': ofp.OFPFW_NW_DST_MASK,
+ 'nw_tos': ofp.OFPFW_NW_TOS,
+ 'nw_proto': ofp.OFPFW_NW_PROTO,
+ 'tp_src': ofp.OFPFW_TP_SRC,
+ 'tp_dst': ofp.OFPFW_TP_DST,
+ }
+ for key in kwargs:
+ setattr(match, key, kwargs[key])
+ match.wildcards &= ~fields[key]
+ return match
+
+ def testPktsAgainstFlow(self, pkts, acts, match):
+ if type(acts) != list:
+ acts = [acts]
+ for info in pkts:
+ title, pkt, expected_result = info
+ self.testPktAgainstFlow(title, pkt, acts, match, expected_result)
+
+ def testPktAgainstFlow(self, title, pkt, acts, match, expected_result):
+ of_ports = config["port_map"].keys()
+ of_ports.sort()
+ self.assertTrue(len(of_ports) > 1, "Not enough ports for test")
+
+ rv = delete_all_flows(self.controller)
+ self.assertEqual(rv, 0, "Failed to delete all flows")
+
+ ingress_port = of_ports[0]
+ egress_port = of_ports[1]
+
+ logging.info("Testing packet '%s', expect result %s" %
+ (title, expected_result))
+ logging.info("Ingress %s to egress %s" %
+ (str(ingress_port), str(egress_port)))
+ logging.info("Packet:")
+ logging.info(self.pktToStr(pkt))
+
+ match.in_port = ingress_port
+
+ request = message.flow_mod()
+ request.match = match
+
+ request.buffer_id = 0xffffffff
+ for act in acts:
+ act.port = egress_port
+ rv = request.actions.add(act)
+ self.assertTrue(rv, "Could not add action")
+
+ logging.info("Inserting flow")
+ rv = self.controller.message_send(request)
+ self.assertTrue(rv != -1, "Error installing flow mod")
+ self.assertEqual(do_barrier(self.controller), 0, "Barrier failed")
+
+ logging.info("Sending packet to dp port " +
+ str(ingress_port))
+ self.dataplane.send(ingress_port, str(pkt))
+
+ exp_pkt_arg = None
+ exp_port = None
+ if config["relax"]:
+ exp_pkt_arg = pkt
+ exp_port = egress_port
+
+ (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(port_number=exp_port,
+ exp_pkt=exp_pkt_arg)
+ if expected_result == self.RESULT_MATCH:
+ self.assertTrue(rcv_pkt is not None,
+ "Did not receive packet, expected a match")
+ logging.debug("Packet len " + str(len(rcv_pkt)) + " in on " +
+ str(rcv_port))
+ self.assertEqual(rcv_port, egress_port, "Unexpected receive port")
+ str_pkt = str(pkt)
+ str_rcv_pkt = str(rcv_pkt)
+ str_rcv_pkt = str_rcv_pkt[0:len(str_pkt)]
+ if str_pkt != str_rcv_pkt:
+ logging.error("Response packet does not match send packet")
+ logging.info("Response:")
+ logging.info(self.pktToStr(scapy.Ether(rcv_pkt)))
+ self.assertEqual(str_pkt, str_rcv_pkt,
+ 'Response packet does not match send packet')
+ elif expected_result == self.RESULT_NOMATCH:
+ self.assertTrue(rcv_pkt is None, "Received packet, expected drop")
+ else:
+ logging.debug("Match or drop accepted. Result = %s" %
+ ("match" if rcv_pkt is not None else "drop"))
+
+
+class DirectBadIpTcpPacketsBase(DirectBadPacketBase):
+ """
+ Base class for TCP and UDP parsing/matching verification under corruptions
+ """
+ def runTest(self):
+ pass
+
+ def runTestWithProto(self, protoName = 'TCP'):
+ dl_dst='00:01:02:03:04:05'
+ dl_src='00:06:07:08:09:0a'
+ ip_src='192.168.0.1'
+ ip_dst='192.168.0.2'
+ ip_tos=0
+ tcp_sport=1234
+ tcp_dport=80
+
+ # Generate a proper packet for constructing a match
+ tp = None
+ if protoName == 'TCP':
+ tp = scapy.TCP
+ proto = 6
+ elif protoName == 'UDP':
+ tp = scapy.UDP
+ proto = 17
+ else:
+ raise Exception("Passed in unknown proto name")
+
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos)/ \
+ tp(sport=tcp_sport, dport=tcp_dport)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+
+ def testPacket(title, pkt, result):
+ act = action.action_output()
+ pkts = [
+ [title, pkt, result]
+ ]
+ self.testPktsAgainstFlow(pkts, act, match)
+
+ # Try incomplete IP headers
+ testPacket("Incomplete IP header (1 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:1],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (2 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:2],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (3 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:3],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (12 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:12],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (16 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:16],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (19 bytes)",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto))[0:19],
+ self.RESULT_NOMATCH,
+ )
+
+ # Try variations where the TCP header is missing or incomplete. As we
+ # saw bugs before where buffers were reused and lengths weren't honored,
+ # we initiatlize once with a non-matching full packet and once with a
+ # matching full packet.
+ testPacket("Non-Matching TCP packet, warming buffer",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto)/ \
+ tp(sport=tcp_sport, dport=tcp_dport + 1),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Missing TCP header, buffer warmed with non-match",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Matching TCP packet, warming buffer",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto)/ \
+ tp(sport=tcp_sport, dport=tcp_dport),
+ self.RESULT_MATCH,
+ )
+ testPacket("Missing TCP header, buffer warmed with match",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Truncated TCP header: 2 bytes",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto)/ \
+ (str(tp(sport=tcp_sport, dport=tcp_dport))[0:2]),
+ self.RESULT_NOMATCH,
+ )
+
+ # Play with IP header length values that put the start of TCP either
+ # inside the generated TCP header or beyond. In some cases it may even
+ # be beyond the packet boundary. Also play with IP options and more
+ # importantly IP total length corruptions.
+ testPacket("TCP packet, corrupt ihl (0x6)",
+ simple_tcp_packet(ip_ihl=6),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("TCP packet, corrupt ihl (0xf)",
+ simple_tcp_packet(ip_ihl=0xf), # ihl = 15 * 4 = 60
+ self.RESULT_NOMATCH,
+ )
+ testPacket("TCP packet, corrupt ihl and total length",
+ simple_tcp_packet(ip_ihl=0xf, pktlen=56), # ihl = 15 * 4 = 60,
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Corrupt IPoption: First 4 bytes of matching TCP header",
+ simple_tcp_packet(
+ ip_options=scapy.IPOption('\x04\xd2\x00\x50'),
+ tcp_dport=2, tcp_sport=2
+ ),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Missing TCP header, corrupt ihl",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=0xf, proto=proto),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Missing TCP header, corrupt total length",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto, len= 100),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Missing TCP header, corrupt ihl and total length",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=0xf, proto=proto, len=43),
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (12 bytes), corrupt ihl and total length",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto, ihl=10, len=43))[0:12],
+ self.RESULT_NOMATCH,
+ )
+ testPacket("Incomplete IP header (16 bytes), corrupt ihl and total length",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ str(scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto, ihl=10, len=43))[0:16],
+ self.RESULT_NOMATCH,
+ )
+
+ # Try an incomplete TCP header that has enough bytes to carry source and
+ # destination ports. As that is all we care about during matching, some
+ # implementations may match and some may drop the packet
+ testPacket("Incomplete TCP header: src/dst port present",
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=proto)/ \
+ (str(tp(sport=tcp_sport, dport=tcp_dport))[0:4]),
+ self.RESULT_ANY,
+ )
+
+ for i in range(1):
+ for length in range(40 / 4): # IPv4 options are a maximum of 40 in length
+ bytes = "".join([("%c" % random.randint(0, 255)) for x in range(length * 4)])
+ eth = scapy.Ether(dst=dl_dst, src=dl_src)
+ ip = scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=5 + length, proto=proto)
+ tcp = tp(sport=tcp_sport, dport=tcp_dport+1)
+ pkt = eth / ip
+ pkt = pkt / bytes
+ pkt = pkt / str(tcp)
+ testPacket("Random IP options len = %d - TP match must fail" % length * 4,
+ pkt,
+ self.RESULT_NOMATCH
+ )
+
+ eth = scapy.Ether(dst=dl_dst, src=dl_src)
+ ip = scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, ihl=5 + length, proto=proto)
+ tcp = tp(sport=tcp_sport, dport=tcp_dport)
+ pkt = eth / ip
+ pkt = pkt / bytes
+ pkt = pkt / str(tcp)
+
+ testPacket("Random IP options len = %d - May match",
+ pkt,
+ self.RESULT_ANY
+ )
+
+
+class DirectBadIpTcpPackets(DirectBadIpTcpPacketsBase):
+ """
+ Verify IP/TCP parsing and matching. Focus on packet corruptions
+ """
+ def runTest(self):
+ self.runTestWithProto(protoName = 'TCP')
+
+class DirectBadIpUdpPackets(DirectBadIpTcpPacketsBase):
+ """
+ Verify IP/UDP parsing and matching. Focus on packet corruptions
+ """
+ def runTest(self):
+ self.runTestWithProto(protoName = 'UDP')
+
+class DirectBadLlcPackets(DirectBadPacketBase):
+ """
+ Verify LLC/SNAP parsing and matching. Focus on packet corruptions
+ """
+ def runTest(self):
+ dl_dst='00:01:02:03:04:05'
+ dl_src='00:06:07:08:09:0a'
+ ip_src='192.168.0.1'
+ ip_dst='192.168.0.2'
+ ip_tos=0
+ tcp_sport=1234
+ tcp_dport=80
+
+ IS_SNAP_IP = 1
+ IS_SNAP_IP_CORRUPT = 2
+ IS_NOT_SNAP_IP = 3
+
+ def testPacketTcpMatch(title, llc):
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos)/ \
+ scapy.TCP(sport=tcp_sport, dport=tcp_dport)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+ act = action.action_output()
+
+ self.testPktsAgainstFlow(
+ [[
+ "TCP match - LLC frame correct length - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=len(llc)) / llc,
+ self.RESULT_ANY,
+ ]],
+ act, match
+ )
+
+ # Corrupt length field
+ ethLen = random.randint(0, 1535)
+ self.testPktsAgainstFlow(
+ [[
+ "TCP match - LLC frame corrupted length - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=ethLen) / llc,
+ self.RESULT_ANY,
+ ]],
+ act, match
+ )
+
+ def testPacketEthSrcDstMatch(title, llc):
+ # Matching based on Ethernet source and destination
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+ match.wildcards |= ofp.OFPFW_DL_TYPE
+ self.testPktsAgainstFlow(
+ [[
+ "Eth addr match - LLC frame correct length- %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=len(llc)) / llc,
+ self.RESULT_MATCH,
+ ]],
+ action.action_output(), match
+ )
+
+ # Corrupt length field
+ ethLen = random.randint(0, 1535)
+ self.testPktsAgainstFlow(
+ [[
+ "Eth addr match - LLC frame corrupted length- %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=ethLen) / llc,
+ self.RESULT_ANY,
+ ]],
+ action.action_output(), match
+ )
+
+ def testPacketEthSrcDstTypeMatch(title, llc, is_snap_ip):
+ # Matching based on Ethernet source, destination and type
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src, type=0x800)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+ if is_snap_ip == IS_SNAP_IP:
+ is_match = self.RESULT_MATCH
+ elif is_snap_ip == IS_SNAP_IP_CORRUPT:
+ is_match = self.RESULT_ANY
+ else:
+ is_match = self.RESULT_NOMATCH
+ self.testPktsAgainstFlow(
+ [[
+ "Eth addr+type match - LLC frame correct length - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=len(llc)) / llc,
+ is_match,
+ ]],
+ action.action_output(), match
+ )
+
+ # Corrupt length field
+ ethLen = random.randint(0, 1535)
+ self.testPktsAgainstFlow(
+ [[
+ "Eth addr+type match - LLC frame corrupted length - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=ethLen) / llc,
+ self.RESULT_ANY,
+ ]],
+ action.action_output(), match
+ )
+
+ def testPacket(title, llc, is_snap_ip):
+ testPacketTcpMatch(title, llc)
+ testPacketEthSrcDstMatch(title, llc)
+ testPacketEthSrcDstTypeMatch(title, llc, is_snap_ip)
+
+ testPacket("LLC - No SNAP - No Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - No SNAP - Small Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * 10),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - No SNAP - Max -1 Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * (1500 - 3 - 1)),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - No SNAP - Max Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * (1500 - 3)),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - SNAP - Small bogus payload",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x800) / ("S" * 10),
+ IS_SNAP_IP_CORRUPT,
+ )
+ testPacket("LLC - SNAP - Max -1 bogus payload",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x3) / ("S" * (1500 - 3 - 5 - 1)),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - SNAP - Max bogus payload",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x3) / ("S" * (1500 - 3 - 5)),
+ IS_NOT_SNAP_IP,
+ )
+ testPacket("LLC - SNAP - IP - TCP",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x800)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=6)/ \
+ scapy.TCP(sport=tcp_sport, dport=tcp_dport),
+ IS_SNAP_IP,
+ )
+
+
+class DirectLlcPackets(DirectBadPacketBase):
+ """
+ Verify LLC/SNAP parsing (valid and corrupted packets) and matching
+ """
+ def runTest(self):
+ dl_dst='00:01:02:03:04:05'
+ dl_src='00:06:07:08:09:0a'
+ ip_src='192.168.0.1'
+ ip_dst='192.168.0.2'
+ ip_tos=0
+ tcp_sport=1234
+ tcp_dport=80
+
+ # Test ethertype in face of LLC/SNAP and OFP_DL_TYPE_NOT_ETH_TYPE
+ IS_SNAP_NOT_IP = 1
+ IS_SNAP_AND_IP = 2
+ IS_NOT_SNAP = 3
+
+ def testPacketEthTypeIP(title, llc, is_snap):
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src, type=0x800)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+ pkts = []
+ if is_snap == IS_NOT_SNAP or is_snap == IS_SNAP_NOT_IP:
+ result = self.RESULT_NOMATCH
+ else:
+ result = self.RESULT_MATCH
+ pkts.append([
+ "Ether type 0x800 match - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=len(llc)) / llc,
+ result,
+ ])
+ act = action.action_output()
+ self.testPktsAgainstFlow(pkts, act, match)
+
+ def testPacketEthTypeNotEth(title, llc, is_snap):
+ match_pkt = scapy.Ether(dst = dl_dst, src = dl_src,
+ type = ofp.OFP_DL_TYPE_NOT_ETH_TYPE)
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+ pkts = []
+ if is_snap == IS_NOT_SNAP:
+ result = self.RESULT_MATCH
+ else:
+ result = self.RESULT_NOMATCH
+ pkts.append([
+ "Ether type OFP_DL_TYPE_NOT_ETH_TYPE match - %s" % title,
+ scapy.Ether(dst=dl_dst, src=dl_src, type=len(llc)) / llc,
+ result,
+ ])
+ act = action.action_output()
+ self.testPktsAgainstFlow(pkts, act, match)
+
+ def testPacket(title, llc, is_snap):
+ testPacketEthTypeIP(title, llc, is_snap)
+ testPacketEthTypeNotEth(title, llc, is_snap)
+
+ testPacket("LLC - No SNAP - No Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12),
+ IS_NOT_SNAP,
+ )
+ testPacket("LLC - No SNAP - Small Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * 10),
+ IS_NOT_SNAP,
+ )
+ testPacket("LLC - No SNAP - Max -1 Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * (1500 - 3 - 1)),
+ IS_NOT_SNAP,
+ )
+ testPacket("LLC - No SNAP - Max Payload",
+ scapy.LLC(dsap=0x33, ssap=0x44, ctrl=0x12) / ("S" * (1500 - 3)),
+ IS_NOT_SNAP,
+ )
+ testPacket("LLC - SNAP - Non-default OUI",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000001, code=0x800) / ("S" * 10),
+ IS_NOT_SNAP,
+ )
+ testPacket("LLC - SNAP - Default OUI",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x800) / ("S" * 10),
+ IS_SNAP_AND_IP,
+ )
+ testPacket("LLC - SNAP - Max -1 bogus payload",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x3) / ("S" * (1500 - 3 - 5 - 1)),
+ IS_SNAP_NOT_IP,
+ )
+ testPacket("LLC - SNAP - Max bogus payload",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x3) / ("S" * (1500 - 3 - 5)),
+ IS_SNAP_NOT_IP,
+ )
+ testPacket("LLC - SNAP - IP - TCP",
+ scapy.LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/ \
+ scapy.SNAP(OUI=0x000000, code=0x800)/ \
+ scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos, proto=6)/ \
+ scapy.TCP(sport=tcp_sport, dport=tcp_dport),
+ IS_SNAP_AND_IP,
+ )
+
+
+class DirectArpPackets(DirectBadPacketBase):
+ """
+ Verify ARP parsing (valid and corrupted packets) and ARP matching
+ """
+ def runTest(self):
+ self.testArpHandling()
+
+ def testArpHandling(self):
+ dl_dst='00:01:02:03:04:05'
+ dl_src='00:06:07:08:09:0a'
+ ip_src='192.168.0.1'
+ ip_dst='192.168.0.2'
+ ip_src2='192.168.1.1'
+ ip_dst2='192.168.1.2'
+ ip_tos=0
+ tcp_sport=1234
+ tcp_dport=80
+
+ def testPacket(title, arp_match, arp_pkt, result):
+ pkts = []
+
+ match_pkt = scapy.Ether(dst=dl_dst, src=dl_src) / arp_match
+ match = packet_to_flow_match(self, match_pkt)
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+
+ pkts.append([
+ title,
+ scapy.Ether(dst=dl_dst, src=dl_src) / arp_pkt,
+ result,
+ ])
+
+ act = action.action_output()
+ self.testPktsAgainstFlow(pkts, act, match)
+
+ testPacket("Basic ARP",
+ scapy.ARP(psrc=ip_src, pdst=ip_dst, op = 1),
+ scapy.ARP(hwdst = '00:00:00:00:00:00', hwsrc = dl_src,
+ psrc = ip_src, pdst = ip_dst, hwlen = 6, plen = 4,
+ ptype = 0x800, hwtype = 1, op = 1),
+ self.RESULT_MATCH
+ )
+ # More stuff:
+ # - Non matches on any property
+ # - Corrupted hwlen and plen
+ # - Other hwtype, ptype
+ # - Truncated ARP pkt
+
+
+class DirectVlanPackets(DirectBadPacketBase):
+ """
+ Verify VLAN parsing (valid and corrupted packets) and ARP matching
+ """
+ def runTest(self):
+ dl_dst='00:01:02:03:04:05'
+ dl_src='00:06:07:08:09:0a'
+ ip_src='192.168.0.1'
+ ip_dst='192.168.0.2'
+ ip_src2='192.168.1.1'
+ ip_dst2='192.168.1.2'
+ ip_tos=0
+ tcp_sport=1234
+ tcp_dport=80
+
+ def testPacket(title, match, pkt, result):
+ pkts = []
+
+ self.assertTrue(match is not None,
+ "Could not generate flow match from pkt")
+ match.wildcards &= ~ofp.OFPFW_IN_PORT
+
+ pkts.append([
+ "%s" % title,
+ pkt,
+ result,
+ ])
+
+ act = action.action_output()
+ self.testPktsAgainstFlow(pkts, act, match)
+
+ testPacket("Basic MAC matching - IPv4 payload",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src)),
+ scapy.Ether(dst=dl_dst, src=dl_src, type=0x800) / scapy.IP(),
+ self.RESULT_MATCH
+ )
+ testPacket("Basic MAC matching - VMware beacon - no payload",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src)),
+ scapy.Ether(dst=dl_dst, src=dl_src, type=0x8922),
+ self.RESULT_MATCH
+ )
+ testPacket("Basic MAC matching - VMware beacon - with payload",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src)),
+ scapy.Ether(dst=dl_dst, src=dl_src, type=0x8922)/ ("X" * 1),
+ self.RESULT_MATCH
+ )
+ testPacket("Basic MAC matching - IPv6 payload",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src)),
+ scapy.Ether(dst=dl_dst, src=dl_src) / scapy.IPv6(),
+ self.RESULT_MATCH
+ )
+ testPacket("Basic MAC matching with VLAN tag present",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src)),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.IP(),
+ self.RESULT_MATCH
+ )
+ testPacket("Basic MAC matching with VLAN tag present",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x800),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.IP(),
+ self.RESULT_MATCH
+ )
+ testPacket("Ether matching with VLAN tag present - No type match",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x801),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.IP(),
+ self.RESULT_NOMATCH
+ )
+ testPacket("Ether matching with VLAN tag present - No type match 0x8100",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x8100),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.IP(),
+ self.RESULT_NOMATCH
+ )
+ testPacket("Ether matching with double VLAN tag - Wrong type match",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x800),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.Dot1Q(prio=3, vlan=1005)/ \
+ scapy.IP(),
+ self.RESULT_NOMATCH
+ )
+ testPacket("Ether matching with double VLAN tag - Type match",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x8100),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.Dot1Q(prio=3, vlan=1005)/ \
+ scapy.IP(),
+ self.RESULT_MATCH
+ )
+ testPacket("IP matching - VLAN tag",
+ self.createMatch(dl_dst=parse_mac(dl_dst), dl_src=parse_mac(dl_src),
+ dl_type=0x0800,
+ nw_src=parse_ip(ip_src), nw_dst=parse_ip(ip_dst)),
+ scapy.Ether(dst=dl_dst, src=dl_src)/ \
+ scapy.Dot1Q(prio=5, vlan=1000)/ \
+ scapy.IP(src=ip_src, dst=ip_dst),
+ self.RESULT_MATCH
+ )
+ # XXX:
+ # - Matching on VLAN ID and Prio
+ # - Actions
+
+
+
if __name__ == "__main__":
print "Please run through oft script: ./oft --test_spec=basic"