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()