Repository: secdev/scapy

Issue:
sniff() from stdin and a filter blocks forever
<!--
Things to consider

1.  Please check that you are using the latest scapy version, e.g. installed via:

    `pip install --upgrade git+git://github.com/secdev/scapy`

2.  If you are here to ask a question - please check previous issues and online resources, and consider using gitter instead: <https://gitter.im/secdev/scapy>

3.  Please understand that **this is not a forum** but an issue tracker. The following article explains why you should limit questions asked on Github issues: <https://medium.com/@methane/why-you-must-not-ask-questions-on-github-issues-51d741d83fde>
-->

#### Brief description

When using `sniff()` with `offline=<file-like object>` (e.g. `sys.stdin`) and a BPF filter, the code attempts to pipe the entire file into the tcpdump subprocess before reading from its stdout. If the input is non-terminating, `sniff()` blocks forever.

#### Environment

-   Scapy version: `32211b495ee2335265c2e13fc92fb1262b6e3175`
-   Python version: `3.6.9`
-   Operating System: `Ubuntu 18.04`

#### How to reproduce

Use any script that generates packets forever on stdout, and another that reads packets from stdin:
```py
# write.py: writes one packet per second to stdout forever
import sys
import time
from scapy.all import *

out = PcapWriter(sys.stdout.buffer)

while 1:
    out.write(IP()/UDP()/"test")
    out.flush()
    time.sleep(1)
```

```py
# read.py: reads packets from stdin with a filter applied
import sys, time
from scapy.all import *

sniff(offline=sys.stdin.buffer, filter='udp', prn=lambda p: p.summary())
```

Then pipe from one script to another:
```sh
python3 write.py | python3 read.py
```

#### Actual result

```sh
$ python write.py | python read.py 
<blocks forever>
```

#### Expected result

```sh
$ python write.py | python read.py 
reading from file -, link-type IPV4 (Raw IPv4)
IP / UDP / DNS Qry 
IP / UDP / DNS Qry 
IP / UDP / DNS Qry 
...
```

When using `sniff()` without the BPF filter, the expected output is produced, because tcpdump is not used in this case.



Discussion:

Resolving patch:
diff --git a/scapy/utils.py b/scapy/utils.py
--- a/scapy/utils.py
+++ b/scapy/utils.py
@@ -1794,6 +1794,9 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, flt=None,
         if prog[0] == conf.prog.wireshark:
             # Start capturing immediately (-k) from stdin (-i -)
             read_stdin_opts = ["-ki", "-"]
+        elif prog[0] == conf.prog.tcpdump:
+            # Capture in packet-buffered mode (-U) from stdin (-r -)
+            read_stdin_opts = ["-U", "-r", "-"]
         else:
             read_stdin_opts = ["-r", "-"]
     else:
@@ -1831,26 +1834,37 @@ def tcpdump(pktlist=None, dump=False, getfd=False, args=None, flt=None,
                 stderr=stderr,
             )
     else:
-        # pass the packet stream
-        with ContextManagerSubprocess(prog[0], suppress=_suppress):
-            proc = subprocess.Popen(
-                prog + read_stdin_opts + args,
-                stdin=subprocess.PIPE,
-                stdout=stdout,
-                stderr=stderr,
-            )
+        try:
+            pktlist.fileno()
+            # pass the packet stream
+            with ContextManagerSubprocess(prog[0], suppress=_suppress):
+                proc = subprocess.Popen(
+                    prog + read_stdin_opts + args,
+                    stdin=pktlist,
+                    stdout=stdout,
+                    stderr=stderr,
+                )
+        except (AttributeError, ValueError):
+            # write the packet stream to stdin
+            with ContextManagerSubprocess(prog[0], suppress=_suppress):
+                proc = subprocess.Popen(
+                    prog + read_stdin_opts + args,
+                    stdin=subprocess.PIPE,
+                    stdout=stdout,
+                    stderr=stderr,
+                )
             if proc is None:
                 # An error has occurred
                 return
-        try:
-            proc.stdin.writelines(iter(lambda: pktlist.read(1048576), b""))
-        except AttributeError:
-            wrpcap(proc.stdin, pktlist, linktype=linktype)
-        except UnboundLocalError:
-            # The error was handled by ContextManagerSubprocess
-            pass
-        else:
-            proc.stdin.close()
+            try:
+                proc.stdin.writelines(iter(lambda: pktlist.read(1048576), b""))
+            except AttributeError:
+                wrpcap(proc.stdin, pktlist, linktype=linktype)
+            except UnboundLocalError:
+                # The error was handled by ContextManagerSubprocess
+                pass
+            else:
+                proc.stdin.close()
     if proc is None:
         # An error has occurred
         return


Test patch:
diff --git a/test/regression.uts b/test/regression.uts
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -7366,7 +7366,7 @@ with mock.patch('subprocess.Popen', return_value=Bunch(
         tcpdump([pkt], linktype="DLT_EN3MB", use_tempfile=False)
 
 popen.assert_called_once_with(
-    [conf.prog.tcpdump, '-y', 'EN3MB', '-r', '-'],
+    [conf.prog.tcpdump, '-y', 'EN3MB', '-U', '-r', '-'],
     stdin=subprocess.PIPE, stdout=None, stderr=None)
 
 print(bytes_hex(f.getvalue()))
@@ -7387,7 +7387,7 @@ with mock.patch('subprocess.Popen', return_value=Bunch(
         tcpdump([pkt], linktype=scapy.data.DLT_EN10MB, use_tempfile=False)
 
 popen.assert_called_once_with(
-    [conf.prog.tcpdump, '-y', 'EN10MB', '-r', '-'],
+    [conf.prog.tcpdump, '-y', 'EN10MB', '-U', '-r', '-'],
     stdin=subprocess.PIPE, stdout=None, stderr=None)
 
 print(bytes_hex(f.getvalue()))