byt3bl33d3r
Published

Sun 19 July 2015

←Home

Mad-Max Scapy: Improving Scapy's packet sending performance

I've been using Scapy for years and one thing that's always bothered me was it's performace, especially when it comes to sending packets, to give you an idea:

from scapy.all import *
for i in range(0, 10):
    send(ARP(pdst='192.168.1.88', 
             psrc='192.168.1.11', 
             hwdst='f0:84:2f:fb:a8:89', 
             op='is-at'), 
        iface='enp3s0', verbose=False)
    print 'sent packet'

Simple enough, were just sending 10 ARP packets:

time sudo python normal_send.py
WARNING: No route found for IPv6 destination :: (no default route?)
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sudo -E python normal_send.py  0.38s user 0.04s system 32% cpu 1.311 total

So it took us 1.311 seconds to send 10 packets?? Boooo! That's way too long! Surely there must be something we can do to speed things up a bit!

After downloading the source, I took a look at the send() function in the sendrecv.py file:

@conf.commands.register
def send(x, inter=0, loop=0, count=None, verbose=None, realtime=None, *args, **kargs):
"""Send packets at layer 3 send(packets, [inter=0], [loop=0], [verbose=conf.verb]) -> None"""
__gen_send(conf.L3socket(*args, **kargs), x, inter=inter, loop=loop, count=count,verbose=verbose, realtime=realtime)

Well ok, looks like it's creating a socket and passing it the function arguments, lets take a look at __gen_send():

def __gen_send(s, x, inter=0, loop=0, count=None, verbose=None, realtime=None, *args, **kargs):
    if type(x) is str:
        x = conf.raw_layer(load=x)
    if not isinstance(x, Gen):
        x = SetGen(x)
    if verbose is None:
        verbose = conf.verb
    n = 0
    if count is not None:
        loop = -count
    elif not loop:
        loop=-1
    try:
        while loop:
            dt0 = None
            for p in x:
                if realtime:
                    ct = time.time()
                    if dt0:
                        st = dt0+p.time-ct
                        if st > 0:
                            time.sleep(st)
                    else:
                        dt0 = ct-p.time 
                s.send(p)
                n += 1
                if verbose:
                    os.write(1,".")
                time.sleep(inter)
            if loop < 0:
                loop += 1
    except KeyboardInterrupt:
        pass
    s.close()
    if verbose:
        print "\nSent %i packets." % n

At first glance, nothing out of the ordinary: just doing some basic type checking, setting some instance variables and looping if the loop argument is set. At second glance I noticed that pesky s.close()! It's closing the socket!

Every time you invoke send() or sendp() Scapy will automatically create and close a socket for every packet you send! I can see convenience in that, makes the API much simpler! But I'm willing to bet that definitely takes a hit on performance!

Grep'ing the source, we can see other places where it's used:

scapy/config.py:342:    L3socket = None
scapy/supersocket.py:138:if conf.L3socket is None:
scapy/supersocket.py:139:    conf.L3socket = L3RawSocket
scapy/sendrecv.py:251:    __gen_send(conf.L3socket(*args, **kargs), x, inter=inter, loop=loop, count=count,verbose=verbose, realtime=realtime)
scapy/sendrecv.py:316:    s = conf.L3socket(filter=filter, iface=iface, nofilter=nofilter)
scapy/sendrecv.py:334:    s=conf.L3socket(filter=filter, nofilter=nofilter, iface=iface)
scapy/sendrecv.py:507:    s = conf.L3socket(filter=filter, iface=iface, nofilter=nofilter)
scapy/arch/pcapdnet.py:364:    conf.L3socket=L3dnetSocket
scapy/arch/linux.py:517:conf.L3socket = L3PacketSocket
scapy/layers/inet6.py:2946:  if not conf.L3socket == _IPv6inIP:
scapy/layers/inet6.py:2947:    _IPv6inIP.cls = conf.L3socket
scapy/layers/inet6.py:2949:    del(conf.L3socket)
scapy/layers/inet.py:1493:    s=conf.L3socket()
scapy/scapypipes.py:83:        self.s = conf.L3socket(iface=self.iface)
scapy/automaton.py:440:        self.send_sock_class = kargs.pop("ll", conf.L3socket)
doc/scapy/troubleshooting.rst:19:    >>> conf.L3socket
doc/scapy/troubleshooting.rst:21:    >>> conf.L3socket=L3RawSocket
doc/scapy/usage.rst:508:    >>> conf.L3socket=L3dnetSocket

Cool! Judging from scapypipes.py line 83 , all we need to do to open a socket is:

s = conf.L3socket(iface=interface)

Let's try it out!

from scapy.all import *

s = conf.L3socket(iface='enp3s0')
for i in range(0, 10):
    s.send(ARP(pdst='192.168.1.88', 
               psrc='192.168.1.11', 
               hwdst='f0:84:2f:fb:a8:89', 
               op='is-at')
          )
    print 'sent packet'

And when run:

time sudo python madmax_send.py
WARNING: No route found for IPv6 destination :: (no default route?)
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sent packet
sudo -E python madmax_send.py  0.36s user 0.02s system 82% cpu 0.464 total

Holy packets Batman! That's almost 2x the speed! \o/

Go Top