Skip to content

Commit

Permalink
Native Mac OS X and *BSD support (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
guedou authored and p-l- committed Dec 2, 2016
1 parent a63c377 commit 35f97ef
Show file tree
Hide file tree
Showing 16 changed files with 893 additions and 33 deletions.
15 changes: 10 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ matrix:
- os: linux
python: pypy

- os: osx
language: generic

- os: osx
language: generic
env:
Expand All @@ -25,21 +28,23 @@ matrix:
sudo: required
python: 2.7
env:
- TRAVIS_SUDO=sudo
- SCAPY_USE_PCAPDNET=yes
- TRAVIS_SUDO=sudo TEST_COMBINED_MODES=yes

- os: linux
sudo: required
python: 2.7
env:
- TRAVIS_SUDO=sudo
- TEST_COMBINED_MODES=yes
- TRAVIS_SUDO=sudo SCAPY_USE_PCAPDNET=yes

- os: osx
language: generic
env:
- TRAVIS_SUDO=sudo
- SCAPY_USE_PCAPDNET=yes

- os: osx
language: generic
env:
- TRAVIS_SUDO=sudo SCAPY_USE_PCAPDNET=yes

install: bash .travis/install.sh

Expand Down
18 changes: 15 additions & 3 deletions .travis/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,34 @@ python -c "from scapy.all import *; print conf"
# Don't run tests that require root privileges
if [ -z "$TRAVIS_SUDO" -o "$TRAVIS_SUDO" = "false" ]
then
UT_FLAGS="-K netaccess "
UT_FLAGS="-K netaccess -K needs_root"
TRAVIS_SUDO=""
fi

# Test AEAD modes in IPsec if available
if [ "$TEST_COMBINED_MODES" != "yes" ]
then
UT_FLAGS+="-K combined_modes "
UT_FLAGS+=" -K combined_modes "
fi

# Run unit tests
cd test/

if [ "$TRAVIS_OS_NAME" = "osx" ]
then
if [ -z $SCAPY_USE_PCAPDNET ]
then
$TRAVIS_SUDO ./run_tests -q -F -t bpf.uts $UT_FLAGS || exit $?
fi
fi

for f in *.uts
do
$TRAVIS_SUDO ./run_tests -f text -t $f $UT_FLAGS || exit $?
if [ "$f" = "bpf.uts" ]
then
continue
fi
$TRAVIS_SUDO ./run_tests -q -F -t $f $UT_FLAGS || exit $?
done

for f in ../scapy/contrib/*.uts
Expand Down
17 changes: 14 additions & 3 deletions scapy/arch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def str2mac(s):
return ("%02x:"*6)[:-1] % tuple(map(ord, s))


if not scapy.config.conf.use_pcap and not scapy.config.conf.use_dnet:
from scapy.arch.bpf.core import get_if_raw_addr

def get_if_addr(iff):
return socket.inet_ntoa(get_if_raw_addr(iff))

Expand All @@ -63,6 +66,7 @@ def get_if_hwaddr(iff):
# def attach_filter(s, filter, iface):
# def set_promisc(s,iff,val=1):
# def read_routes():
# def read_routes6():
# def get_if(iff,cmd):
# def get_if_index(iff):

Expand All @@ -74,9 +78,16 @@ def get_if_hwaddr(iff):
from scapy.arch.pcapdnet import *
elif BSD:
from scapy.arch.unix import read_routes, read_routes6, in6_getifaddr
scapy.config.conf.use_pcap = True
scapy.config.conf.use_dnet = True
from scapy.arch.pcapdnet import *

if scapy.config.conf.use_pcap or scapy.config.conf.use_dnet:
from scapy.arch.pcapdnet import *
else:
from scapy.arch.bpf.supersocket import L2bpfListenSocket, L2bpfSocket, L3bpfSocket
from scapy.arch.bpf.core import *
scapy.config.conf.use_bpf = True
scapy.config.conf.L2listen = L2bpfListenSocket
scapy.config.conf.L2socket = L2bpfSocket
scapy.config.conf.L3socket = L3bpfSocket
elif SOLARIS:
from scapy.arch.solaris import *
elif WINDOWS:
Expand Down
5 changes: 5 additions & 0 deletions scapy/arch/bpf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Guillaume Valadon <guillaume@valadon.net>

"""
Scapy *BSD native support
"""
23 changes: 23 additions & 0 deletions scapy/arch/bpf/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Guillaume Valadon <guillaume@valadon.net>

"""
Scapy *BSD native support - constants
"""


from scapy.data import MTU


SIOCGIFFLAGS = 0xc0206911
BPF_BUFFER_LENGTH = MTU

# From net/bpf.h
BIOCIMMEDIATE = 0x80044270
BIOCGSTATS = 0x4008426f
BIOCPROMISC = 0x20004269
BIOCSETIF = 0x8020426c
BIOCSBLEN = 0xc0044266
BIOCGBLEN = 0x40044266
BIOCSETF = 0x80104267
BIOCSHDRCMPLT = 0x80044275
BIOCGDLT = 0x4004426a
223 changes: 223 additions & 0 deletions scapy/arch/bpf/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Guillaume Valadon <guillaume@valadon.net>

"""
Scapy *BSD native support - core
"""

from scapy.config import conf
from scapy.error import Scapy_Exception
from scapy.data import ARPHDR_LOOPBACK, ARPHDR_ETHER
from scapy.arch.common import get_if
from scapy.arch.consts import LOOPBACK_NAME
from scapy.utils import warning

from scapy.arch.bpf.consts import *

import os
import socket
import fcntl
import struct

from ctypes import cdll, cast, pointer, POINTER, Structure
from ctypes import c_uint, c_uint32, c_int, c_ulong, c_char_p, c_ushort, c_ubyte
from ctypes.util import find_library


# ctypes definitions

LIBC = cdll.LoadLibrary(find_library("libc"))
LIBC.ioctl.argtypes = [c_int, c_ulong, c_char_p]
LIBC.ioctl.restype = c_int


class bpf_insn(Structure):
""""The BPF instruction data structure"""
_fields_ = [("code", c_ushort),
("jt", c_ubyte),
("jf", c_ubyte),
("k", c_uint32)]


class bpf_program(Structure):
""""Structure for BIOCSETF"""
_fields_ = [("bf_len", c_uint),
("bf_insns", POINTER(bpf_insn))]


# Addresses manipulation functions

def get_if_raw_addr(ifname):
"""Returns the IPv4 address configured on 'ifname', packed with inet_pton."""

# Get ifconfig output
try:
fd = os.popen("%s %s" % (conf.prog.ifconfig, ifname))
except OSError, msg:
raise Scapy_Exception("Failed to execute ifconfig: (%s)" % msg)

# Get IPv4 addresses
addresses = [l for l in fd.readlines() if l.find("netmask") >= 0]
if not addresses:
raise Scapy_Exception("No IPv4 address found on %s !" % ifname)

# Pack the first address
address = addresses[0].split(' ')[1]
return socket.inet_pton(socket.AF_INET, address)


def get_if_raw_hwaddr(ifname):
"""Returns the packed MAC address configured on 'ifname'."""

NULL_MAC_ADDRESS = '\x00'*6

# Handle the loopback interface separately
if ifname == LOOPBACK_NAME:
return (ARPHDR_LOOPBACK, NULL_MAC_ADDRESS)

# Get ifconfig output
try:
fd = os.popen("%s %s" % (conf.prog.ifconfig, ifname))
except OSError, msg:
warning("Failed to execute ifconfig: (%s)" % msg)
raise Scapy_Exception("Failed to execute ifconfig: (%s)" % msg)

# Get MAC addresses
addresses = [l for l in fd.readlines() if l.find("ether") >= 0 or
l.find("lladdr") >= 0 or
l.find("address") >= 0]
if not addresses:
raise Scapy_Exception("No MAC address found on %s !" % ifname)

# Pack and return the MAC address
mac = addresses[0].split(' ')[1]
mac = [chr(int(b, 16)) for b in mac.split(':')]
return (ARPHDR_ETHER, ''.join(mac))


# BPF specific functions

def get_dev_bpf():
"""Returns an opened BPF file object"""

# Get the first available BPF handle
for bpf in range(0, 8):
try:
fd = os.open("/dev/bpf%i" % bpf, os.O_RDWR)
return (fd, bpf)
except OSError, err:
continue

raise Scapy_Exception("No /dev/bpf handle is available !")


def attach_filter(fd, iface, bpf_filter_string):
"""Attach a BPF filter to the BPF file descriptor"""

# Retrieve the BPF byte code in decimal
command = "%s -i %s -ddd -s 1600 '%s'" % (conf.prog.tcpdump, iface, bpf_filter_string)
try:
f = os.popen(command)
except OSError, msg:
raise Scapy_Exception("Failed to execute tcpdump: (%s)" % msg)

# Convert the byte code to a BPF program structure
lines = f.readlines()
if lines == []:
raise Scapy_Exception("Got an empty BPF filter from tcpdump !")

# Allocate BPF instructions
size = int(lines[0])
bpf_insn_a = bpf_insn * size
bip = bpf_insn_a()

# Fill the BPF instruction structures with the byte code
lines = lines[1:]
for i in xrange(len(lines)):
values = [int(v) for v in lines[i].split()]
bip[i].code = c_ushort(values[0])
bip[i].jt = c_ubyte(values[1])
bip[i].jf = c_ubyte(values[2])
bip[i].k = c_uint(values[3])

# Create the BPF program and assign it to the interface
bp = bpf_program(size, bip)
ret = LIBC.ioctl(c_int(fd), BIOCSETF, cast(pointer(bp), c_char_p))
if ret < 0:
raise Scapy_Exception("Can't attach the BPF filter !")


# Interface manipulation functions

def get_if_list():
"""Returns a list containing all network interfaces."""

# Get ifconfig output
try:
fd = os.popen("%s -a" % conf.prog.ifconfig)
except OSError, msg:
raise Scapy_Exception("Failed to execute ifconfig: (%s)" % msg)

# Get interfaces
interfaces = [line[:line.find(':')] for line in fd.readlines()
if ": flags" in line.lower()]
return interfaces


def get_working_ifaces():
"""
Returns an ordered list of interfaces that could be used with BPF.
Note: the order mimics pcap_findalldevs() behavior
"""

# Only root is allowed to perform the following ioctl() call
if os.getuid() != 0:
return []

# Test all network interfaces
interfaces = []
for ifname in get_if_list():

# Unlike pcap_findalldevs(), we do not care of loopback interfaces.
if ifname == LOOPBACK_NAME:
continue

# Get interface flags
try:
result = get_if(ifname, SIOCGIFFLAGS)
except IOError, msg:
warning("ioctl(SIOCGIFFLAGS) failed on %s !" % ifname)
continue

# Convert flags
ifflags = struct.unpack("16xH14x", result)[0]
if ifflags & 0x1: # IFF_UP

# Get a BPF handle
fd, _ = get_dev_bpf()
if fd is None:
raise Scapy_Exception("No /dev/bpf are available !")

# Check if the interface can be used
try:
fcntl.ioctl(fd, BIOCSETIF, struct.pack("16s16x", ifname))
interfaces.append((ifname, int(ifname[-1])))
except IOError, err:
pass

# Close the file descriptor
os.close(fd)

# Sort to mimic pcap_findalldevs() order
interfaces.sort(lambda (ifname_left, ifid_left),
(ifname_right, ifid_right): ifid_left-ifid_right)
return interfaces


def get_working_if():
"""Returns the first interface than can be used with BPF"""

ifaces = get_working_ifaces()
if not ifaces:
# A better interface will be selected later using the routing table
return LOOPBACK_NAME
return ifaces[0][0]
Loading

0 comments on commit 35f97ef

Please sign in to comment.