blob: 7d04e18672bd3b3813511f8ec829ea903c38dd98 [file] [log] [blame]
# Guillaume Valadon <guillaume@valadon.net>
"""
Scapy *BSD native support - BPF sockets
"""
import errno
import fcntl
import os
from select import select
import struct
import time
from scapy.arch.bpf.core import get_dev_bpf, attach_filter
from scapy.arch.bpf.consts import BIOCGBLEN, BIOCGDLT, BIOCGSTATS, \
BIOCIMMEDIATE, BIOCPROMISC, BIOCSBLEN, BIOCSETIF, BIOCSHDRCMPLT, \
BPF_BUFFER_LENGTH
from scapy.config import conf
from scapy.consts import FREEBSD, NETBSD
from scapy.data import ETH_P_ALL
from scapy.error import Scapy_Exception, warning
from scapy.supersocket import SuperSocket
from scapy.compat import raw
if FREEBSD or NETBSD:
BPF_ALIGNMENT = 8 # sizeof(long)
else:
BPF_ALIGNMENT = 4 # sizeof(int32_t)
# SuperSockets definitions
class _L2bpfSocket(SuperSocket):
""""Generic Scapy BPF Super Socket"""
desc = "read/write packets using BPF"
assigned_interface = None
fd_flags = None
ins = None
closed = False
def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, nofilter=0):
# SuperSocket mandatory variables
if promisc is None:
self.promisc = conf.sniff_promisc
else:
self.promisc = promisc
if iface is None:
self.iface = conf.iface
else:
self.iface = iface
# Get the BPF handle
(self.ins, self.dev_bpf) = get_dev_bpf()
self.outs = self.ins
# Set the BPF buffer length
try:
fcntl.ioctl(self.ins, BIOCSBLEN, struct.pack('I', BPF_BUFFER_LENGTH))
except IOError:
raise Scapy_Exception("BIOCSBLEN failed on /dev/bpf%i" %
self.dev_bpf)
# Assign the network interface to the BPF handle
try:
fcntl.ioctl(self.ins, BIOCSETIF, struct.pack("16s16x", self.iface.encode()))
except IOError:
raise Scapy_Exception("BIOCSETIF failed on %s" % self.iface)
self.assigned_interface = self.iface
# Set the interface into promiscuous
if self.promisc:
self.set_promisc(1)
# Don't block on read
try:
fcntl.ioctl(self.ins, BIOCIMMEDIATE, struct.pack('I', 1))
except IOError:
raise Scapy_Exception("BIOCIMMEDIATE failed on /dev/bpf%i" %
self.dev_bpf)
# Scapy will provide the link layer source address
# Otherwise, it is written by the kernel
try:
fcntl.ioctl(self.ins, BIOCSHDRCMPLT, struct.pack('i', 1))
except IOError:
raise Scapy_Exception("BIOCSHDRCMPLT failed on /dev/bpf%i" %
self.dev_bpf)
# Configure the BPF filter
if not nofilter:
if conf.except_filter:
if filter:
filter = "(%s) and not (%s)" % (filter, conf.except_filter)
else:
filter = "not (%s)" % conf.except_filter
if filter is not None:
attach_filter(self.ins, self.iface, filter)
# Set the guessed packet class
self.guessed_cls = self.guess_cls()
def set_promisc(self, value):
"""Set the interface in promiscuous mode"""
try:
fcntl.ioctl(self.ins, BIOCPROMISC, struct.pack('i', value))
except IOError:
raise Scapy_Exception("Cannot set promiscuous mode on interface "
"(%s)!" % self.iface)
def __del__(self):
"""Close the file descriptor on delete"""
# When the socket is deleted on Scapy exits, __del__ is
# sometimes called "too late", and self is None
if self is not None:
self.close()
def guess_cls(self):
"""Guess the packet class that must be used on the interface"""
# Get the data link type
try:
ret = fcntl.ioctl(self.ins, BIOCGDLT, struct.pack('I', 0))
ret = struct.unpack('I', ret)[0]
except IOError:
cls = conf.default_l2
warning("BIOCGDLT failed: unable to guess type. Using %s !",
cls.name)
return cls
# Retrieve the corresponding class
try:
return conf.l2types[ret]
except KeyError:
cls = conf.default_l2
warning("Unable to guess type (type %i). Using %s", ret, cls.name)
def set_nonblock(self, set_flag=True):
"""Set the non blocking flag on the socket"""
# Get the current flags
if self.fd_flags is None:
try:
self.fd_flags = fcntl.fcntl(self.ins, fcntl.F_GETFL)
except IOError:
warning("Cannot get flags on this file descriptor !")
return
# Set the non blocking flag
if set_flag:
new_fd_flags = self.fd_flags | os.O_NONBLOCK
else:
new_fd_flags = self.fd_flags & ~os.O_NONBLOCK
try:
fcntl.fcntl(self.ins, fcntl.F_SETFL, new_fd_flags)
self.fd_flags = new_fd_flags
except:
warning("Can't set flags on this file descriptor !")
def get_stats(self):
"""Get received / dropped statistics"""
try:
ret = fcntl.ioctl(self.ins, BIOCGSTATS, struct.pack("2I", 0, 0))
return struct.unpack("2I", ret)
except IOError:
warning("Unable to get stats from BPF !")
return (None, None)
def get_blen(self):
"""Get the BPF buffer length"""
try:
ret = fcntl.ioctl(self.ins, BIOCGBLEN, struct.pack("I", 0))
return struct.unpack("I", ret)[0]
except IOError:
warning("Unable to get the BPF buffer length")
return
def fileno(self):
"""Get the underlying file descriptor"""
return self.ins
def close(self):
"""Close the Super Socket"""
if not self.closed and self.ins is not None:
os.close(self.ins)
self.closed = True
self.ins = None
def send(self, x):
"""Dummy send method"""
raise Exception("Can't send anything with %s" % self.__name__)
def recv(self, x=BPF_BUFFER_LENGTH):
"""Dummy recv method"""
raise Exception("Can't recv anything with %s" % self.__name__)
class L2bpfListenSocket(_L2bpfSocket):
""""Scapy L2 BPF Listen Super Socket"""
received_frames = []
def buffered_frames(self):
"""Return the number of frames in the buffer"""
return len(self.received_frames)
def get_frame(self):
"""Get a frame or packet from the received list"""
if self.received_frames:
return self.received_frames.pop(0)
@staticmethod
def bpf_align(bh_h, bh_c):
"""Return the index to the end of the current packet"""
# from <net/bpf.h>
return ((bh_h + bh_c)+(BPF_ALIGNMENT-1)) & ~(BPF_ALIGNMENT-1)
def extract_frames(self, bpf_buffer):
"""Extract all frames from the buffer and stored them in the received list."""
# Ensure that the BPF buffer contains at least the header
len_bb = len(bpf_buffer)
if len_bb < 20: # Note: 20 == sizeof(struct bfp_hdr)
return
# Extract useful information from the BPF header
if FREEBSD or NETBSD:
# struct bpf_xhdr or struct bpf_hdr32
bh_tstamp_offset = 16
else:
# struct bpf_hdr
bh_tstamp_offset = 8
# Parse the BPF header
bh_caplen = struct.unpack('I', bpf_buffer[bh_tstamp_offset:bh_tstamp_offset+4])[0]
next_offset = bh_tstamp_offset + 4
bh_datalen = struct.unpack('I', bpf_buffer[next_offset:next_offset+4])[0]
next_offset += 4
bh_hdrlen = struct.unpack('H', bpf_buffer[next_offset:next_offset+2])[0]
if bh_datalen == 0:
return
# Get and store the Scapy object
frame_str = bpf_buffer[bh_hdrlen:bh_hdrlen+bh_caplen]
try:
pkt = self.guessed_cls(frame_str)
except:
if conf.debug_dissector:
raise
pkt = conf.raw_layer(frame_str)
self.received_frames.append(pkt)
# Extract the next frame
end = self.bpf_align(bh_hdrlen, bh_caplen)
if (len_bb - end) >= 20:
self.extract_frames(bpf_buffer[end:])
def recv(self, x=BPF_BUFFER_LENGTH):
"""Receive a frame from the network"""
if self.buffered_frames():
# Get a frame from the buffer
return self.get_frame()
# Get data from BPF
try:
bpf_buffer = os.read(self.ins, x)
except EnvironmentError as exc:
if exc.errno != errno.EAGAIN:
warning("BPF recv()", exc_info=True)
return
# Extract all frames from the BPF buffer
self.extract_frames(bpf_buffer)
return self.get_frame()
class L2bpfSocket(L2bpfListenSocket):
""""Scapy L2 BPF Super Socket"""
def send(self, x):
"""Send a frame"""
return os.write(self.outs, raw(x))
def nonblock_recv(self):
"""Non blocking receive"""
if self.buffered_frames():
# Get a frame from the buffer
return self.get_frame()
# Set the non blocking flag, read from the socket, and unset the flag
self.set_nonblock(True)
pkt = L2bpfListenSocket.recv(self)
self.set_nonblock(False)
return pkt
class L3bpfSocket(L2bpfSocket):
def get_frame(self):
"""Get a frame or packet from the received list"""
pkt = super(L3bpfSocket, self).get_frame()
if pkt is not None:
return pkt.payload
def send(self, pkt):
"""Send a packet"""
# Use the routing table to find the output interface
iff = pkt.route()[0]
if iff is None:
iff = conf.iface
# Assign the network interface to the BPF handle
if self.assigned_interface != iff:
try:
fcntl.ioctl(self.outs, BIOCSETIF, struct.pack("16s16x", iff.encode()))
except IOError:
raise Scapy_Exception("BIOCSETIF failed on %s" % iff)
self.assigned_interface = iff
# Build the frame
frame = raw(self.guessed_cls()/pkt)
pkt.sent_time = time.time()
# Send the frame
L2bpfSocket.send(self, frame)
# Sockets manipulation functions
def isBPFSocket(obj):
"""Return True is obj is a BPF Super Socket"""
return isinstance(obj, L2bpfListenSocket) or isinstance(obj, L2bpfListenSocket) or isinstance(obj, L3bpfSocket)
def bpf_select(fds_list, timeout=None):
"""A call to recv() can return several frames. This functions hides the fact
that some frames are read from the internal buffer."""
# Check file descriptors types
bpf_scks_buffered = list()
select_fds = list()
for tmp_fd in fds_list:
# Specific BPF sockets: get buffers status
if isBPFSocket(tmp_fd) and tmp_fd.buffered_frames():
bpf_scks_buffered.append(tmp_fd)
continue
# Regular file descriptors or empty BPF buffer
select_fds.append(tmp_fd)
if select_fds:
# Call select for sockets with empty buffers
if timeout is None:
timeout = 0.05
ready_list, _, _ = select(select_fds, [], [], timeout)
return bpf_scks_buffered + ready_list
else:
return bpf_scks_buffered