Repository: secdev/scapy

Issue:
sendpfast() not working with Python 2.7
#### Brief description

Part of the changes from 2.3 to 2.4 included `sendpfast()` changing from using `subprocess.check_call()` to `subprocess.Popen()`.

The `subprocess.Popen()` call is using a context object form for automatic closing of the handle:

```python
        with subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as cmd:  # noqa: E501
            stdout, stderr = cmd.communicate()
```

I believe this context object form wasn't introduced to Popen until Python 3.

In Python 2.7, this raises `AttributeError: __exit__`, which causes an error message in interactive mode and the packet is not sent.

#### Environment

-   Scapy version: confirmed with both 2.4.2 release as well as latest master `e5a9b82f`
-   Python version: `Python 2.7.14`
-   Operating System: Docker image based on `Debian GNU/Linux 8 (jessie)`

#### How to reproduce

```bash
docker run --rm -it python:2 bash
apt-get update && apt-get install -y tcpreplay
pip install scapy==2.4.2
scapy
sendpfast(Ether() / IP(proto='udp', dst='233.43.202.24') / UDP(dport=11124) / 'TEST', iface='eth0')
```

#### Actual result

```
INFO: ['tcpreplay', '--intf1=eth0', '--topspeed', '/tmp/scapyoGcRCI']
ERROR: Cannot execute [tcpreplay]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/scapy/sendrecv.py", line 371, in sendpfast
    with subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as cmd:  # noqa: E501
AttributeError: __exit__
```

#### Expected result

```
WARNING: Mac address to reach destination not found. Using broadcast.
.
Sent 1 packets.
```



Discussion:

Resolving patch:
diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py
--- a/scapy/sendrecv.py
+++ b/scapy/sendrecv.py
@@ -11,6 +11,7 @@
 import itertools
 import threading
 import os
+import re
 import socket
 import subprocess
 import time
@@ -21,7 +22,8 @@
 from scapy.config import conf
 from scapy.error import warning
 from scapy.packet import Packet, Gen
-from scapy.utils import get_temp_file, PcapReader, tcpdump, wrpcap
+from scapy.utils import get_temp_file, tcpdump, wrpcap, \
+    ContextManagerSubprocess, PcapReader
 from scapy import plist
 from scapy.error import log_runtime, log_interactive
 from scapy.base_classes import SetGen
@@ -366,25 +368,25 @@ def sendpfast(x, pps=None, mbps=None, realtime=None, loop=0, file_cache=False, i
     argv.append(f)
     wrpcap(f, x)
     results = None
-    try:
-        log_runtime.info(argv)
-        with subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as cmd:  # noqa: E501
+    with ContextManagerSubprocess("sendpfast()", conf.prog.tcpreplay):
+        try:
+            cmd = subprocess.Popen(argv, stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        except KeyboardInterrupt:
+            log_interactive.info("Interrupted by user")
+        except Exception:
+            os.unlink(f)
+            raise
+        else:
             stdout, stderr = cmd.communicate()
-            log_runtime.info(stdout)
-            log_runtime.warning(stderr)
+            if stderr:
+                log_runtime.warning(stderr.decode())
             if parse_results:
                 results = _parse_tcpreplay_result(stdout, stderr, argv)
-
-    except KeyboardInterrupt:
-        log_interactive.info("Interrupted by user")
-    except Exception:
-        if conf.interactive:
-            log_interactive.error("Cannot execute [%s]", argv[0], exc_info=True)  # noqa: E501
-        else:
-            raise
-    finally:
-        os.unlink(f)
-        return results
+            elif conf.verb > 2:
+                log_runtime.info(stdout.decode())
+    os.unlink(f)
+    return results
 
 
 def _parse_tcpreplay_result(stdout, stderr, argv):
@@ -398,26 +400,46 @@ def _parse_tcpreplay_result(stdout, stderr, argv):
     :return: dictionary containing the results
     """
     try:
-        results_dict = {}
-        stdout = plain_str(stdout).replace("\nRated: ", "\t\tRated: ").replace("\t", "").split("\n")  # noqa: E501
-        stderr = plain_str(stderr).replace("\t", "").split("\n")
-        actual = [x for x in stdout[0].split(" ") if x]
-
-        results_dict["packets"] = int(actual[1])
-        results_dict["bytes"] = int(actual[3][1:])
-        results_dict["time"] = float(actual[7])
-        results_dict["bps"] = float(actual[10])
-        results_dict["mbps"] = float(actual[12])
-        results_dict["pps"] = float(actual[14])
-        results_dict["attempted"] = int(stdout[2].split(" ")[-1:][0])
-        results_dict["successful"] = int(stdout[3].split(" ")[-1:][0])
-        results_dict["failed"] = int(stdout[4].split(" ")[-1:][0])
-        results_dict["retried_enobufs"] = int(stdout[5].split(" ")[-1:][0])
-        results_dict["retried_eagain"] = int(stdout[6].split(" ")[-1][0])
-        results_dict["command"] = str(argv)
-        results_dict["warnings"] = stderr[:len(stderr) - 1]
-        return results_dict
+        results = {}
+        stdout = plain_str(stdout).lower()
+        stderr = plain_str(stderr).strip().split("\n")
+        elements = {
+            "actual": (int, int, float),
+            "rated": (float, float, float),
+            "flows": (int, float, int, int),
+            "attempted": (int,),
+            "successful": (int,),
+            "failed": (int,),
+            "truncated": (int,),
+            "retried packets (eno": (int,),
+            "retried packets (eag": (int,),
+        }
+        multi = {
+            "actual": ("packets", "bytes", "time"),
+            "rated": ("bps", "mbps", "pps"),
+            "flows": ("flows", "fps", "flow_packets", "non_flow"),
+            "retried packets (eno": ("retried_enobufs",),
+            "retried packets (eag": ("retried_eagain",),
+        }
+        float_reg = r"([0-9]*\.[0-9]+|[0-9]+)"
+        int_reg = r"([0-9]+)"
+        any_reg = r"[^0-9]*"
+        r_types = {int: int_reg, float: float_reg}
+        for line in stdout.split("\n"):
+            line = line.strip()
+            for elt, _types in elements.items():
+                if line.startswith(elt):
+                    regex = any_reg.join([r_types[x] for x in _types])
+                    matches = re.search(regex, line)
+                    for i, typ in enumerate(_types):
+                        name = multi.get(elt, [elt])[i]
+                        results[name] = typ(matches.group(i + 1))
+        results["command"] = " ".join(argv)
+        results["warnings"] = stderr[:-1]
+        return results
     except Exception as parse_exception:
+        if not conf.interactive:
+            raise
         log_runtime.error("Error parsing output: " + str(parse_exception))
         return {}
 


Test patch:
diff --git a/test/regression.uts b/test/regression.uts
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -6973,7 +6973,8 @@ test_openbsd_6_3()
 
 from scapy.sendrecv import _parse_tcpreplay_result
 
-stdout = """Actual: 1024 packets (198929 bytes) sent in 67.88 seconds.              Rated: 2930.6 bps, 0.02 Mbps, 15.09 pps
+stdout = """Actual: 1024 packets (198929 bytes) sent in 67.88 seconds.
+Rated: 2930.6 bps, 0.02 Mbps, 15.09 pps
 Statistics for network device: mon0
         Attempted packets:         1024
         Successful packets:        1024
@@ -6986,7 +6987,7 @@ Unsupported physical layer type 0x0323 on mon0.  Maybe it works, maybe it won't.
 sending out mon0
 processing file: replay-example.pcap"""
 
-argv = ['tcpreplay' '--intf1=mon0' '--multiplier=1.00' '--timer=nano' 'replay-example.pcap']
+argv = ['tcpreplay', '--intf1=mon0', '--multiplier=1.00', '--timer=nano', 'replay-example.pcap']
 results_dict = _parse_tcpreplay_result(stdout, stderr, argv)
 
 results_dict
@@ -7002,9 +7003,46 @@ assert(results_dict["successful"] == 1024)
 assert(results_dict["failed"] == 0)
 assert(results_dict["retried_enobufs"] == 0)
 assert(results_dict["retried_eagain"] == 0)
-assert(results_dict["command"] == str(argv))
+assert(results_dict["command"] == " ".join(argv))
 assert(len(results_dict["warnings"]) == 3)
 
+= Test more recent version with flows
+
+data = """Actual: 1 packets (42 bytes) sent in 0.000278 seconds
+Rated: 151079.1 Bps, 1.20 Mbps, 3597.12 pps
+Flows: 1 flows, 3597.12 fps, 1 flow packets, 0 non-flow
+Statistics for network device: enp0s3
+        Successful packets:        1
+        Failed packets:            0
+        Truncated packets:         0
+        Retried packets (ENOBUFS): 0
+        Retried packets (EAGAIN):  0
+"""
+
+results_dict = _parse_tcpreplay_result(data, "", [])
+results_dict
+
+expected = {
+    'bps': 151079.1,
+    'bytes': 42,
+    'command': '',
+    'failed': 0,
+    'flow_packets': 1,
+    'flows': 1,
+    'fps': 3597.12,
+    'mbps': 1.2,
+    'non_flow': 0,
+    'packets': 1,
+    'pps': 3597.12,
+    'retried_eagain': 0,
+    'retried_enobufs': 0,
+    'successful': 1,
+    'time': 0.000278,
+    'truncated': 0,
+    'warnings': []
+}
+
+assert results_dict == expected
 
 ############
 ############