Scapy is an incredible tool when it comes to playing with the network. As it is written on its official website, Scapy can replace a majority of network tools such as nmap, hping and tcpdump.

One of the features offered by Scapy is to sniff the network packets passing through a computer's NIC. Below is a small example:

from scapy.all import *

interface = "eth0"

def print_packet(packet):
    ip_layer = packet.getlayer(IP)
    print("[!] New Packet: {src} -> {dst}".format(src=ip_layer.src, dst=ip_layer.dst))

print("[*] Start sniffing...")
sniff(iface=interface, filter="ip", prn=print_packet)
print("[*] Stop sniffing")

This little sniffer displays the source and the destination of all packets having an IP layer:

$ sudo python3 sniff_main_thread.py
[*] Start sniffing...
[!] New Packet: 10.137.2.30 -> 10.137.2.1
[!] New Packet: 10.137.2.30 -> 10.137.2.1
[!] New Packet: 10.137.2.1 -> 10.137.2.30
[!] New Packet: 10.137.2.1 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
^C[*] Stop sniffing

It will continue to sniff network packets until it receives a keyboard interruption (CTRL+C).

Now, let's look at a new example:

from scapy.all import *
from threading import Thread
from time import sleep

class Sniffer(Thread):
    def  __init__(self, interface="eth0"):
        super().__init__()

        self.interface = interface

    def run(self):
        sniff(iface=self.interface, filter="ip", prn=self.print_packet)

    def print_packet(self, packet):
        ip_layer = packet.getlayer(IP)
        print("[!] New Packet: {src} -> {dst}".format(src=ip_layer.src, dst=ip_layer.dst))

sniffer = Sniffer()

print("[*] Start sniffing...")
sniffer.start()

try:
    while True:
        sleep(100)
except KeyboardInterrupt:
    print("[*] Stop sniffing")
    sniffer.join()

This piece of code does exactly the same thing as the previous one except that this time the sniff function is executed inside a dedicated thread. Everything works well with this new version except when it comes to stopping the sniffer:

$ sudo python3 sniff_thread_issue.py
[*] Start sniffing...
[!] New Packet: 10.137.2.30 -> 10.137.2.1
[!] New Packet: 10.137.2.30 -> 10.137.2.1
[!] New Packet: 10.137.2.1 -> 10.137.2.30
[!] New Packet: 10.137.2.1 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 10.137.2.30 -> 216.58.198.68
[!] New Packet: 216.58.198.68 -> 10.137.2.30
[!] New Packet: 10.137.2.30 -> 216.58.198.68
^C[*] Stop sniffing
^CTraceback (most recent call last):
  File "sniff_thread_issue.py", line 25, in <module>
    sleep(100)
KeyboardInterrupt

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "sniff_thread_issue.py", line 28, in <module>
    sniffer.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt
^CException ignored in: <module 'threading' from '/usr/lib/python3.5/threading.py'>
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 1288, in _shutdown
    t.join()
  File "/usr/lib/python3.5/threading.py", line 1054, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.5/threading.py", line 1070, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

When CTRL+C is pressed, a SIGTERM signal is sent to the process executing the Python script, triggering its exit routine. However, as said in the official documentation about signals, only the main thread receives signals:

Python signal handlers are always executed in the main Python thread, even if the signal was received in another thread.

As a result, when CTRL+C is pressed, only the main thread raises a KeyboardInterrupt exception. The sniffing thread will continue its infinite sniffing loop, blocking at the same time the call of sniffer.join().

So, how can the sniffing thread be stopped if not by signals? Let's have a look at this next example:

from scapy.all import *
from threading import Thread, Event
from time import sleep

class Sniffer(Thread):
    def  __init__(self, interface="eth0"):
        super().__init__()

        self.interface = interface
        self.stop_sniffer = Event()

    def run(self):
        sniff(iface=self.interface, filter="ip", prn=self.print_packet, stop_filter=self.should_stop_sniffer)

    def join(self, timeout=None):
        self.stop_sniffer.set()
        super().join(timeout)

    def should_stop_sniffer(self, packet):
        return self.stop_sniffer.isSet()

    def print_packet(self, packet):
        ip_layer = packet.getlayer(IP)
        print("[!] New Packet: {src} -> {dst}".format(src=ip_layer.src, dst=ip_layer.dst))

sniffer = Sniffer()

print("[*] Start sniffing...")
sniffer.start()

try:
    while True:
        sleep(100)
except KeyboardInterrupt:
    print("[*] Stop sniffing")
    sniffer.join()

As you may have noticed, we are now using the stop_filter parameter in the sniff function call. This parameter expects to receive a function which will be called after each new packet to evaluate if the sniffer should continue its job or not. An Event object named stop_sniffer is used for that purpose. It is set to true when the join method is called to stop the thread.

Is this the end of the story? Not really...

$ sudo python3 sniff_thread_issue_2.py
[*] Start sniffing...
^C[*] Stop sniffing
[!] New Packet: 10.137.2.30 -> 10.137.2.1

One side effect remains. Because the should_stop_sniffer method is called only once after each new packet, if it returns false, the sniffer will continue its job, going back to its infinite sniffing loop. This is why the sniffer stopped one packet ahead of the keyboard interruption.

A solution would be to force the sniffing thread to stop. As explained in the official documentation about threading, it is possible to flag a thread as a daemon thread for that purpose:

A thread can be flagged as a “daemon thread”. The significance of this flag is that the entire Python program exits when only daemon threads are left. The initial value is inherited from the creating thread. The flag can be set through the daemon property or the daemon constructor argument.

However, even if this solution would work, the thread won't release the resources it might hold:

Daemon threads are abruptly stopped at shutdown. Their resources (such as open files, database transactions, etc.) may not be released properly. If you want your threads to stop gracefully, make them non-daemonic and use a suitable signalling mechanism such as an Event.

The sniff function uses a socket which is released just before exiting, after the sniffing loop:

try:
    while sniff_sockets:
        // Sniffing loop
except KeyboardInterrupt:
    pass
if opened_socket is None:
    for s in sniff_sockets:
        s.close()
return plist.PacketList(lst,"Sniffed")

Therefore, the solution I suggest is to open the socket outside the sniff function and to give it to this last one as parameter. Consequently, it would be possible to force-stop the sniffing thread while closing its socket properly:

from scapy.all import *
from threading import Thread, Event
from time import sleep

class Sniffer(Thread):
    def  __init__(self, interface="eth0"):
        super().__init__()

        self.daemon = True

        self.socket = None
        self.interface = interface
        self.stop_sniffer = Event()

    def run(self):
        self.socket = conf.L2listen(
            type=ETH_P_ALL,
            iface=self.interface,
            filter="ip"
        )

        sniff(
            opened_socket=self.socket,
            prn=self.print_packet,
            stop_filter=self.should_stop_sniffer
        )

    def join(self, timeout=None):
        self.stop_sniffer.set()
        super().join(timeout)

    def should_stop_sniffer(self, packet):
        return self.stop_sniffer.isSet()

    def print_packet(self, packet):
        ip_layer = packet.getlayer(IP)
        print("[!] New Packet: {src} -> {dst}".format(src=ip_layer.src, dst=ip_layer.dst))

sniffer = Sniffer()

print("[*] Start sniffing...")
sniffer.start()

try:
    while True:
        sleep(100)
except KeyboardInterrupt:
    print("[*] Stop sniffing")
    sniffer.join(2.0)

    if sniffer.isAlive():
        sniffer.socket.close()

Et voilà! The sniffing thread now waits for 2 seconds after having received a keyboard interrupt, letting the time to the sniff function to terminate its job by itself, after which the sniffing thread will be force-stopped and its socket properly closed from the main thread.