Repository: secdev/scapy

Issue:
sniff(offline=...) call blocks when output is large
#### Brief description

When using `sniff()` with `offline` set to a packet list, and the expected output is fairly large, then the call blocks.

This seems to be related to how `scapy.utils.tcpdump()` writes the entire pcap to `tcpdump`'s `stdin` synchronously, and then reads the entire output from `stdout`.

#### Environment

-   Scapy version: `2.4.2`
-   Python version: `3.7.2`
-   Operating System: `Linux (Debian buster)`

#### How to reproduce

When the size of the returned packet is small, there is no problem:

```
>>> sniff(offline=IP()/UDP(sport=(10000, 20000), dport=30000), filter='port 10500')
reading from file -, link-type IPV4 (Raw IPv4)
<Sniffed: TCP:0 UDP:1 ICMP:0 Other:0>
>>> 
```

But with the same input, when the output is expected to be fairly large the function call never returns:
```
>>> sniff(offline=IP()/UDP(sport=(10000, 20000), dport=30000), filter='udp')
reading from file -, link-type IPV4 (Raw IPv4)
```

```
$ ps xa | grep tcpdump
24212 pts/2    S+     0:00 tcpdump -r - -w - udp
```

```
(gdb) bt
#0  0x00007f44e3701804 in __GI___libc_write (fd=1, buf=0x55962e536de0, nbytes=4096) at ../sysdeps/unix/sysv/linux/write.c:26
#1  0x00007f44e369262d in _IO_new_file_write (f=0x7f44e37d3760 <_IO_2_1_stdout_>, data=0x55962e536de0, n=4096) at fileops.c:1183
#2  0x00007f44e36919cf in new_do_write (fp=0x7f44e37d3760 <_IO_2_1_stdout_>, data=0x55962e536de0 "", to_do=to_do@entry=4096) at libioP.h:839
```



Discussion:
I can reproduce the issue. I will investigate a fix.

I found two other issues when `tcpdump` is not available. They are triggered with:
sniff(offline=IP()/TCP(), filter="tcp")
sniff(offline=IP()/TCP())


Resolving patch:
diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py
--- a/scapy/sendrecv.py
+++ b/scapy/sendrecv.py
@@ -20,7 +20,7 @@
 from scapy.data import ETH_P_ALL
 from scapy.config import conf
 from scapy.error import warning
-from scapy.packet import Gen
+from scapy.packet import Gen, Packet
 from scapy.utils import get_temp_file, tcpdump, wrpcap, \
     ContextManagerSubprocess, PcapReader
 from scapy.plist import PacketList, SndRcvList
@@ -772,7 +772,8 @@ def _run(self,
                 message = "tcpdump is not available. Cannot use filter!"
                 raise Scapy_Exception(message)
 
-            if isinstance(offline, list):
+            if isinstance(offline, list) and \
+                    all(isinstance(elt, str) for elt in offline):
                 sniff_sockets.update((PcapReader(
                     fname if flt is None else
                     tcpdump(fname, args=["-w", "-", flt], getfd=True)
@@ -783,6 +784,18 @@ def _run(self,
                     tcpdump(fname, args=["-w", "-", flt], getfd=True)
                 ), label) for fname, label in six.iteritems(offline))
             else:
+                # Write Scapy Packet objects to a pcap file
+                def _write_to_pcap(packets_list):
+                    filename = get_temp_file(autoext=".pcap")
+                    wrpcap(filename, offline)
+                    return filename, filename
+
+                if isinstance(offline, Packet):
+                    tempfile_written, offline = _write_to_pcap([offline])
+                elif isinstance(offline, list) and \
+                        all(isinstance(elt, Packet) for elt in offline):
+                    tempfile_written, offline = _write_to_pcap(offline)
+
                 sniff_sockets[PcapReader(
                     offline if flt is None else
                     tcpdump(offline, args=["-w", "-", flt], getfd=True)


Test patch:
diff --git a/test/regression.uts b/test/regression.uts
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -6728,6 +6728,20 @@ fdesc.close()
 assert list(pktpcap[TCP]) == list(pktpcap_tcp)
 os.unlink(filename)
 
+= Check offline sniff() with Packets and tcpdump
+~ tcpdump
+
+l = sniff(offline=IP()/UDP(sport=(10000, 10001)), filter="udp")
+assert len(l) == 2
+assert(all(UDP in p for p in l))
+
+l = sniff(offline=[p for p in IP()/UDP(sport=(10000, 10001))], filter="udp")
+assert len(l) == 2
+assert(all(UDP in p for p in l))
+
+l = sniff(offline=IP()/UDP(sport=(10000, 10001)), filter="tcp")
+assert len(l) == 0
+
 = Check offline sniff() without a tcpdump binary
 ~ tcpdump
 import mock