Merge into master from pull request #168:
Avoid using libpcap on Linux (https://github.com/floodlight/oftest/pull/168)
diff --git a/README.md b/README.md
index 8790d26..c82a096 100644
--- a/README.md
+++ b/README.md
@@ -64,7 +64,6 @@
 
  * Python 2.7
  * Scapy
- * pypcap (optional - VLAN tests will fail without this)
  * tcpdump (optional - Scapy will complain if it's missing)
  * Doxygen and doxypy (optional - for documentation generation)
  * PyLint (optional - for source checking)
diff --git a/src/python/oftest/afpacket.py b/src/python/oftest/afpacket.py
new file mode 100644
index 0000000..8f6d9de
--- /dev/null
+++ b/src/python/oftest/afpacket.py
@@ -0,0 +1,112 @@
+"""
+AF_PACKET receive support
+
+When VLAN offload is enabled on the NIC Linux will not deliver the VLAN tag
+in the data returned by recv. Instead, it delivers the VLAN TCI in a control
+message. Python 2.x doesn't have built-in support for recvmsg, so we have to
+use ctypes to call it. The recv function exported by this module reconstructs
+the VLAN tag if it was offloaded.
+"""
+
+import socket
+import struct
+from ctypes import *
+
+ETH_P_8021Q = 0x8100
+SOL_PACKET = 263
+PACKET_AUXDATA = 8
+TP_STATUS_VLAN_VALID = 1 << 4
+
+class struct_iovec(Structure):
+    _fields_ = [
+        ("iov_base", c_void_p),
+        ("iov_len", c_size_t),
+    ]
+
+class struct_msghdr(Structure):
+    _fields_ = [
+        ("msg_name", c_void_p),
+        ("msg_namelen", c_uint32),
+        ("msg_iov", POINTER(struct_iovec)),
+        ("msg_iovlen", c_size_t),
+        ("msg_control", c_void_p),
+        ("msg_controllen", c_size_t),
+        ("msg_flags", c_int),
+    ]
+
+class struct_cmsghdr(Structure):
+    _fields_ = [
+        ("cmsg_len", c_size_t),
+        ("cmsg_level", c_int),
+        ("cmsg_type", c_int),
+    ]
+
+class struct_tpacket_auxdata(Structure):
+    _fields_ = [
+        ("tp_status", c_uint),
+        ("tp_len", c_uint),
+        ("tp_snaplen", c_uint),
+        ("tp_mac", c_ushort),
+        ("tp_net", c_ushort),
+        ("tp_vlan_tci", c_ushort),
+        ("tp_padding", c_ushort),
+    ]
+
+libc = CDLL("libc.so.6")
+recvmsg = libc.recvmsg
+recvmsg.argtypes = [c_int, POINTER(struct_msghdr), c_int]
+recvmsg.retype = c_int
+
+def enable_auxdata(sk):
+    """
+    Ask the kernel to return the VLAN tag in a control message
+
+    Must be called on the socket before afpacket.recv.
+    """
+    sk.setsockopt(SOL_PACKET, PACKET_AUXDATA, 1)
+
+def recv(sk, bufsize):
+    """
+    Receive a packet from an AF_PACKET socket
+    @sk Socket
+    @bufsize Maximum packet size
+    """
+    buf = create_string_buffer(bufsize)
+
+    ctrl_bufsize = sizeof(struct_cmsghdr) + sizeof(struct_tpacket_auxdata) + sizeof(c_size_t)
+    ctrl_buf = create_string_buffer(ctrl_bufsize)
+
+    iov = struct_iovec()
+    iov.iov_base = cast(buf, c_void_p)
+    iov.iov_len = bufsize
+
+    msghdr = struct_msghdr()
+    msghdr.msg_name = None
+    msghdr.msg_namelen = 0
+    msghdr.msg_iov = pointer(iov)
+    msghdr.msg_iovlen = 1
+    msghdr.msg_control = cast(ctrl_buf, c_void_p)
+    msghdr.msg_controllen = ctrl_bufsize
+    msghdr.msg_flags = 0
+
+    rv = recvmsg(sk.fileno(), byref(msghdr), 0)
+    if rv < 0:
+        raise RuntimeError("recvmsg failed: rv=%d", rv)
+
+    # The kernel only delivers control messages we ask for. We
+    # only enabled PACKET_AUXDATA, so we can assume it's the
+    # only control message.
+    assert msghdr.msg_controllen >= sizeof(struct_cmsghdr)
+
+    cmsghdr = struct_cmsghdr.from_buffer(ctrl_buf) # pylint: disable=E1101
+    assert cmsghdr.cmsg_level == SOL_PACKET
+    assert cmsghdr.cmsg_type == PACKET_AUXDATA
+
+    auxdata = struct_tpacket_auxdata.from_buffer(ctrl_buf, sizeof(struct_cmsghdr)) # pylint: disable=E1101
+
+    if auxdata.tp_vlan_tci != 0 or auxdata.tp_status & TP_STATUS_VLAN_VALID:
+        # Insert VLAN tag
+        tag = struct.pack("!HH", ETH_P_8021Q, auxdata.tp_vlan_tci)
+        return buf.raw[:12] + tag + buf.raw[12:rv]
+    else:
+        return buf.raw[:rv]
diff --git a/src/python/oftest/dataplane.py b/src/python/oftest/dataplane.py
index 8cda932..81e5929 100644
--- a/src/python/oftest/dataplane.py
+++ b/src/python/oftest/dataplane.py
@@ -27,14 +27,10 @@
 import netutils
 from pcap_writer import PcapWriter
 
-have_pypcap = False
-try:
+if "linux" in sys.platform:
+    import afpacket
+else:
     import pcap
-    if hasattr(pcap, "pcap"):
-        # the incompatible pylibpcap library masquerades as pcap
-        have_pypcap = True
-except:
-    pass
 
 def match_exp_pkt(exp_pkt, pkt):
     """
@@ -50,7 +46,7 @@
     return e == p
 
 
-class DataPlanePort:
+class DataPlanePortLinux:
     """
     Uses raw sockets to capture and send packets on a network interface.
     """
@@ -66,6 +62,7 @@
         self.interface_name = interface_name
         self.socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
                                     socket.htons(self.ETH_P_ALL))
+        afpacket.enable_auxdata(self.socket)
         self.socket.bind((interface_name, 0))
         netutils.set_promisc(self.socket, interface_name)
         self.socket.settimeout(self.RCV_TIMEOUT)
@@ -85,7 +82,7 @@
         Receive a packet from this port.
         @retval (packet data, timestamp)
         """
-        pkt = self.socket.recv(self.RCV_SIZE_DEFAULT)
+        pkt = afpacket.recv(self.socket, self.RCV_SIZE_DEFAULT)
         return (pkt, time.time())
 
     def send(self, packet):
@@ -111,10 +108,8 @@
 
 class DataPlanePortPcap:
     """
-    Alternate port implementation using libpcap. This is required for recent
-    versions of Linux (such as Linux 3.2 included in Ubuntu 12.04) which
-    offload the VLAN tag, so it isn't in the data returned from a read on a raw
-    socket. libpcap understands how to read the VLAN tag from the kernel.
+    Alternate port implementation using libpcap. This is used by non-Linux
+    operating systems.
     """
 
     def __init__(self, interface_name, port_number):
@@ -188,11 +183,10 @@
         #
         if "dataplane" in self.config and "portclass" in self.config["dataplane"]:
             self.dppclass = self.config["dataplane"]["portclass"]
-        elif have_pypcap:
-            self.dppclass = DataPlanePortPcap
+        elif "linux" in sys.platform:
+            self.dppclass = DataPlanePortLinux
         else:
-            self.logger.warning("Missing pypcap, VLAN tests may fail. See README for installation instructions.")
-            self.dppclass = DataPlanePort
+            self.dppclass = DataPlanePortPcap
 
         self.start()