diff --git a/doc/scapy/advanced_usage.rst b/doc/scapy/advanced_usage.rst index 99436aebd54..c7e6ad012a2 100644 --- a/doc/scapy/advanced_usage.rst +++ b/doc/scapy/advanced_usage.rst @@ -790,6 +790,7 @@ Two methods are hooks to be overloaded: * The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition. +.. _pipetools: PipeTools ========= @@ -882,7 +883,10 @@ class ConsoleSink(Sink) Sources ^^^^^^^ -A Source is a class that generates some data. They are several source types integrated with Scapy, usable as-is, but you may also create yours. +A Source is a class that generates some data. + +There are several source types integrated with Scapy, usable as-is, but you may +also create yours. Default Source classes ~~~~~~~~~~~~~~~~~~~~~~ @@ -951,25 +955,165 @@ For instance, here is how TransformDrain is implemented:: Sinks ^^^^^ +Sinks are destinations for messages. + +A :py:class:`Sink` receives data like a :py:class:`Drain`, but doesn't send any +messages after it. + +Messages on the low entry come from :py:meth:`~Sink.push`, and messages on the +high entry come from :py:meth:`~Sink.high_push`. + Default Sink classes ~~~~~~~~~~~~~~~~~~~~ -- Sink : does not do anything. This must be extended to create custom sinks -- ConsoleSink : Print messages on low and high entries -- RawConsoleSink : Print messages on low and high entries, using os.write -- TermSink : Print messages on low and high entries on a separate terminal -- QueueSink: Collect messages from high and low entries and queue them. Messages are unqueued with the .recv() method. +.. py:class:: Sink + + Does nothing; interface to extend for custom sinks. + + All sinks have the following constructor parameters: + + :param name: a human-readable name for the element + :type name: str + + All sinks should implement at least one of these methods: + + .. py:method:: push + + Called by :py:class:`PipeEngine` when there is a new message for the + low entry. + + :param msg: The message data + :returns: None + :rtype: None + + .. py:method:: high_push + + Called by :py:class:`PipeEngine` when there is a new message for the + high entry. + + :param msg: The message data + :returns: None + :rtype: None + +.. py:class:: ConsoleSink + + Prints messages on the low and high entries to ``stdout``. + +.. py:class:: RawConsoleSink + + Prints messages on the low and high entries, using :py:func:`os.write`. + + :param newlines: Include a new-line character after printing each packet. + Defaults to True. + :type newlines: bool + +.. py:class:: TermSink + + Prints messages on the low and high entries, on a separate terminal (xterm + or cmd). + + :param keepterm: Leaves the terminal window open after :py:meth:`~Pipe.stop` + is called. Defaults to True. + :type keepterm: bool + :param newlines: Include a new-line character after printing each packet. + Defaults to True. + :type newlines: bool + :param openearly: Automatically starts the terminal when the constructor is + called, rather than waiting for :py:meth:`~Pipe.start`. + Defaults to True. + :type openearly: bool + +.. py:class:: QueueSink + + Collects messages on the low and high entries into a :py:class:`Queue`. + + Messages are dequeued with :py:meth:`recv`. + + Both high and low entries share the same :py:class:`Queue`. + + .. py:method:: recv + + Reads the next message from the queue. + + If no message is available in the queue, returns None. + + :param block: Blocks execution until a packet is available in the queue. + Defaults to True. + :type block: bool + :param timeout: Controls how long to wait if ``block=True``. If None + (the default), this method will wait forever. If a + non-negative number, this is a number of seconds to + wait before giving up (and returning None). + :type timeout: None, int or float + +.. py:class:: WiresharkSink + + Streams :py:class:`Packet` from the low entry to Wireshark. + + Packets are written into a ``pcap`` stream (like :py:class:`WrpcapSink`), + and streamed to a new Wireshark process on its ``stdin``. + + Wireshark is run with the ``-ki -`` arguments, which cause it to treat + ``stdin`` as a capture device. Arguments in :py:attr:`args` will be + appended after this. + + Extends :py:mod:`WrpcapSink`. + + :param linktype: See :py:attr:`WrpcapSink.linktype`. + :type linktype: None or int + :param args: See :py:attr:`args`. + :type args: None or list[str] + + .. py:attribute:: args + + Additional arguments for the Wireshark process. + + This must be either ``None`` (the default), or a ``list`` of ``str``. + + This attribute has no effect after calling :py:meth:`PipeEngine.start`. + + See :manpage:`wireshark(1)` for more details. + +.. py:class:: WrpcapSink + + Writes :py:class:`Packet` on the low entry to a ``pcap`` file. + + Ignores all messages on the high entry. + + .. note:: + + Due to limitations of the ``pcap`` format, all packets **must** be of + the same link type. This class will not mutate packets to conform with + the expected link type. + + :param fname: Filename to write packets to. + :type fname: str + :param linktype: See :py:attr:`linktype`. + :type linktype: None or int + + .. py:attribute:: linktype + + Set an explicit link-type (``DLT_``) for packets. This must be an + ``int`` or ``None``. + + This is the same as the :py:func:`wrpcap` ``linktype`` parameter. + + If ``None`` (the default), the linktype will be auto-detected on the + first packet. This field will *not* be updated with the result of this + auto-detection. + + This attribute has no effect after calling :py:meth:`PipeEngine.start`. + Create a custom Sink ~~~~~~~~~~~~~~~~~~~~ -To create a custom sink, one must extend the ``Sink`` class. +To create a custom sink, one must extend :py:class:`Sink` and implement +:py:meth:`~Sink.push` and/or :py:meth:`~Sink.high_push`. -A ``Sink`` class receives data like a ``Drain``, from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method. +This is a simplified version of :py:class:`ConsoleSink`: -A ``Sink`` is the dead end of data, it won't be sent anywhere after it. - -For instance, here is how ConsoleSink is implemented:: +.. code-block:: python3 class ConsoleSink(Sink): def push(self, msg): @@ -980,32 +1124,44 @@ For instance, here is how ConsoleSink is implemented:: Link objects ------------ -As shown in the example, most sources can be linked to any drain, on both lower and higher canals. +As shown in the example, most sources can be linked to any drain, on both low +and high entry. -The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one. +The use of ``>`` indicates a link on the low entry, and ``>>`` on the high +entry. -For instance +For example, to link ``a``, ``b`` and ``c`` on the low entries: ->>> a = CLIFeeder() ->>> b = Drain() ->>> c = ConsoleSink() ->>> a > b > c ->>> p = PipeEngine() ->>> p.add(a) +.. code-block:: pycon + + >>> a = CLIFeeder() + >>> b = Drain() + >>> c = ConsoleSink() + >>> a > b > c + >>> p = PipeEngine() + >>> p.add(a) + +This wouldn't link the high entries, so something like this would do nothing: + +.. code-block:: pycon + + >>> a2 = CLIHighFeeder() + >>> a2 >> b + >>> a2.send("hello") -This links a, b, and c on the lower canal. If you tried to send anything on the higher canal, for instance by adding +Because ``b`` (:py:class:`Drain`) and ``c`` (:py:class:`ConsoleSink`) are not +linked on the high entry. ->>> a2 = CLIHighFeeder() ->>> a2 >> b ->>> a2.send("hello") +However, using a :py:class:`DownDrain` would bring the high messages from +:py:class:`CLIHighFeeder` to the lower channel: -It would not do anything as the Drain is not linked to the Sink on the upper canal. However, one could do +.. code-block:: pycon ->>> a2 = CLIHighFeeder() ->>> b2 = DownDrain() ->>> a2 >> b2 ->>> b2 > b ->>> a2.send("hello") + >>> a2 = CLIHighFeeder() + >>> b2 = DownDrain() + >>> a2 >> b2 + >>> b2 > b + >>> a2.send("hello") The PipeEngine class -------------------- diff --git a/doc/scapy/conf.py b/doc/scapy/conf.py index 81b5de10d9c..cbe968db5fa 100644 --- a/doc/scapy/conf.py +++ b/doc/scapy/conf.py @@ -106,6 +106,8 @@ ] } +# Make :manpage directive work on HTML output. +manpages_url = 'https://manpages.debian.org/{path}' # -- Options for HTMLHelp output ------------------------------------------ diff --git a/doc/scapy/usage.rst b/doc/scapy/usage.rst index 4dbd4fa4345..8dd4ae3b8ed 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -1533,28 +1533,76 @@ Viewing packets with Wireshark Problem ^^^^^^^ -You have generated or sniffed some packets with Scapy and want to view them with `Wireshark `_, because of its advanced packet dissection abilities. +You have generated or sniffed some packets with Scapy. + +Now you want to view them with `Wireshark `_, because +of its advanced packet dissection capabilities. Solution ^^^^^^^^ -That's what the ``wireshark()`` function is for: +That's what :py:func:`wireshark` is for! + +.. code-block:: python3 - >>> packets = Ether()/IP(dst=Net("google.com/30"))/ICMP() # first generate some packets - >>> wireshark(packets) # show them with Wireshark + # First, generate some packets... + packets = IP(src="192.0.2.9", dst=Net("192.0.2.10/30"))/ICMP() -Wireshark will start in the background and show your packets. + # Show them with Wireshark + wireshark(packets) + +Wireshark will start in the background, and show your packets. Discussion ^^^^^^^^^^ -The ``wireshark()`` function generates a temporary pcap-file containing your packets, starts Wireshark in the background and makes it read the file on startup. +.. py:function:: wireshark(pktlist, ...) + + With a :py:class:`Packet` or :py:class:`PacketList`, serialises your + packets, and streams this into Wireshark via ``stdin`` as if it were a + capture device. + + Because this uses ``pcap`` format to serialise the packets, there are some + limitations: + + * Packets must be all of the same ``linktype``. + + For example, you can't mix :py:class:`Ether` and :py:class:`IP` at the + top layer. + + * Packets must have an assigned (and supported) ``DLT_*`` constant for the + ``linktype``. An unsupported ``linktype`` is replaced with ``DLT_EN10MB`` + (Ethernet), and will display incorrectly in Wireshark. + + For example, can't pass a bare :py:class:`ICMP` packet, but you can send + it as a payload of an :py:class:`IP` or :py:class:`IPv6` packet. + + With a filename (passed as a string), this loads the given file in + Wireshark. This needs to be in a format that Wireshark supports. + + You can tell Scapy where to find the Wireshark executable by changing the + ``conf.prog.wireshark`` configuration setting. + + This accepts the same extra parameters as :py:func:`tcpdump`. + +.. seealso:: + + :py:class:`WiresharkSink` + A :ref:`PipeTools sink ` for live-streaming packets. -Please remember that Wireshark works with Layer 2 packets (usually called "frames"). So we had to add an ``Ether()`` header to our ICMP packets. Passing just IP packets (layer 3) to Wireshark will give strange results. + :manpage:`wireshark(1)` + Additional description of Wireshark's functionality, and its + command-line arguments. -You can tell Scapy where to find the Wireshark executable by changing the ``conf.prog.wireshark`` configuration setting. + `Wireshark's website`__ + For up-to-date releases of Wireshark. + `Wireshark Protocol Reference`__ + Contains detailed information about Wireshark's protocol dissectors, and + reference documentation for various network protocols. +__ https://www.wireshark.org +__ https://wiki.wireshark.org/ProtocolReference OS Fingerprinting ----------------- diff --git a/scapy/pipetool.py b/scapy/pipetool.py index 9c3232c54f3..60b320fa0d0 100644 --- a/scapy/pipetool.py +++ b/scapy/pipetool.py @@ -652,12 +652,11 @@ def push(self, msg): def high_push(self, msg): self.q.put(msg) - def recv(self): - while True: - try: - return self.q.get(True, timeout=0.1) - except six.moves.queue.Empty: - pass + def recv(self, block=True, timeout=None): + try: + return self.q.get(block=block, timeout=timeout) + except six.moves.queue.Empty: + pass class TransformDrain(Drain): diff --git a/scapy/scapypipes.py b/scapy/scapypipes.py index 3dbb65b7cc8..5c36df23cbc 100644 --- a/scapy/scapypipes.py +++ b/scapy/scapypipes.py @@ -5,34 +5,54 @@ from __future__ import print_function import socket +import subprocess + from scapy.modules.six.moves.queue import Queue, Empty from scapy.pipetool import Source, Drain, Sink from scapy.config import conf from scapy.compat import raw -from scapy.utils import PcapReader, PcapWriter +from scapy.utils import ContextManagerSubprocess, PcapReader, PcapWriter from scapy.automaton import recv_error from scapy.consts import WINDOWS class SniffSource(Source): """Read packets from an interface and send them to low exit. - +-----------+ - >>-| |->> - | | - >-| [iface]--|-> - +-----------+ -""" - def __init__(self, iface=None, filter=None, name=None): + +-----------+ + >>-| |->> + | | + >-| [iface]--|-> + +-----------+ + + If neither of the ``iface`` or ``socket`` parameters are specified, then + Scapy will capture from the first network interface. + + :param iface: A layer 2 interface to sniff packets from. Mutually + exclusive with the ``socket`` parameter. + :param filter: Packet filter to use while capturing. See ``L2listen``. + Not used with ``socket`` parameter. + :param socket: A ``SuperSocket`` to sniff packets from. + """ + + def __init__(self, iface=None, filter=None, socket=None, name=None): Source.__init__(self, name=name) + + if (iface or filter) and socket: + raise ValueError("iface and filter options are mutually exclusive " + "with socket") + + self.s = socket self.iface = iface self.filter = filter def start(self): - self.s = conf.L2listen(iface=self.iface, filter=self.filter) + if not self.s: + self.s = conf.L2listen(iface=self.iface, filter=self.filter) def stop(self): - self.s.close() + if self.s: + self.s.close() def fileno(self): return self.s.fileno() @@ -125,16 +145,56 @@ class WrpcapSink(Sink): +----------+ """ - def __init__(self, fname, name=None): + def __init__(self, fname, name=None, linktype=None): Sink.__init__(self, name=name) - self.f = PcapWriter(fname) + self.fname = fname + self.f = None + self.linktype = linktype + + def start(self): + self.f = PcapWriter(self.fname, linktype=self.linktype) def stop(self): - self.f.flush() - self.f.close() + if self.f: + self.f.flush() + self.f.close() def push(self, msg): - self.f.write(msg) + if msg: + self.f.write(msg) + + +class WiresharkSink(WrpcapSink): + """Packets received on low input are pushed to Wireshark. + + +----------+ + >>-| |->> + | | + >-|--[pcap] |-> + +----------+ + """ + + def __init__(self, name=None, linktype=None, args=None): + WrpcapSink.__init__(self, fname=None, name=name, linktype=linktype) + self.args = args + + def start(self): + # Wireshark must be running first, because PcapWriter will block until + # data has been read! + with ContextManagerSubprocess("WiresharkSink", conf.prog.wireshark): + args = [conf.prog.wireshark, "-ki", "-"] + if self.args: + args.extend(self.args) + + proc = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=None, + stderr=None, + ) + + self.fname = proc.stdin + WrpcapSink.start(self) class UDPDrain(Drain): diff --git a/scapy/utils.py b/scapy/utils.py index a65c0fda649..af7c2e90ffd 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -48,16 +48,41 @@ def issubtype(x, t): return isinstance(x, type) and issubclass(x, t) -def get_temp_file(keep=False, autoext=""): - """Create a temporary file and return its name. When keep is False, - the file is deleted when scapy exits. +def get_temp_file(keep=False, autoext="", fd=False): + """Creates a temporary file. + :param keep: If False, automatically delete the file when Scapy exits. + :param autoext: Suffix to add to the generated file name. + :param fd: If True, this returns a file-like object with the temporary + file opened. If False (default), this returns a file path. """ - fname = tempfile.NamedTemporaryFile(prefix="scapy", suffix=autoext, - delete=False).name + f = tempfile.NamedTemporaryFile(prefix="scapy", suffix=autoext, + delete=False) if not keep: - conf.temp_files.append(fname) - return fname + conf.temp_files.append(f.name) + + if fd: + return f + else: + # Close the file so something else can take it. + f.close() + return f.name + + +def get_temp_dir(keep=False): + """Creates a temporary file, and returns its name. + + :param keep: If False (default), the directory will be recursively + deleted when Scapy exits. + :return: A full path to a temporary directory. + """ + + dname = tempfile.mkdtemp(prefix="scapy") + + if not keep: + conf.temp_files.append(dname) + + return dname def sane_color(x): @@ -1253,9 +1278,12 @@ def _write_header(self, pkt): self.f.flush() def write(self, pkt): - """accepts either a single packet or a list of packets to be - written to the dumpfile + """ + Writes a Packet or bytes to a pcap file. + :param pkt: Packet(s) to write (one record for each Packet), or raw + bytes to write (as one record). + :type pkt: iterable[Packet], Packet or bytes """ if isinstance(pkt, bytes): if not self.header_present: @@ -1263,25 +1291,35 @@ def write(self, pkt): self._write_packet(pkt) else: pkt = pkt.__iter__() - if not self.header_present: - try: - p = next(pkt) - except (StopIteration, RuntimeError): - self._write_header(None) - return - self._write_header(p) - self._write_packet(p) for p in pkt: + if not self.header_present: + self._write_header(p) self._write_packet(p) - def _write_packet(self, packet, sec=None, usec=None, caplen=None, wirelen=None): # noqa: E501 - """writes a single packet to the pcap file + def _write_packet(self, packet, sec=None, usec=None, caplen=None, + wirelen=None): + """ + Writes a single packet to the pcap file. + + :param packet: bytes for a single packet + :type packet: bytes + :param sec: time the packet was captured, in seconds since epoch. If + not supplied, defaults to now. + :type sec: int or long + :param usec: If ``nano=True``, then number of nanoseconds after the + second that the packet was captured. If ``nano=False``, + then the number of microseconds after the second the + packet was captured + :type usec: int or long + :param caplen: The length of the packet in the capture file. If not + specified, uses ``len(packet)``. + :type caplen: int + :param wirelen: The length of the packet on the wire. If not + specified, uses ``caplen``. + :type wirelen: int + :returns: None + :rtype: None """ - if isinstance(packet, tuple): - for pkt in packet: - self._write_packet(pkt, sec=sec, usec=usec, caplen=caplen, - wirelen=wirelen) - return if caplen is None: caplen = len(packet) if wirelen is None: @@ -1291,9 +1329,13 @@ def _write_packet(self, packet, sec=None, usec=None, caplen=None, wirelen=None): it = int(t) if sec is None: sec = it - if usec is None: - usec = int(round((t - it) * (1000000000 if self.nano else 1000000))) # noqa: E501 - self.f.write(struct.pack(self.endian + "IIII", sec, usec, caplen, wirelen)) # noqa: E501 + usec = int(round((t - it) * + (1000000000 if self.nano else 1000000))) + elif usec is None: + usec = 0 + + self.f.write(struct.pack(self.endian + "IIII", + sec, usec, caplen, wirelen)) self.f.write(packet) if self.sync: self.f.flush() @@ -1302,6 +1344,8 @@ def flush(self): return self.f.flush() def close(self): + if not self.header_present: + self._write_header(None) return self.f.close() def __enter__(self): @@ -1316,8 +1360,6 @@ class PcapWriter(RawPcapWriter): """A stream PCAP writer with more control than wrpcap()""" def _write_header(self, pkt): - if isinstance(pkt, tuple) and pkt: - pkt = pkt[0] if self.linktype is None: try: self.linktype = conf.l2types[pkt.__class__] @@ -1326,17 +1368,51 @@ def _write_header(self, pkt): self.linktype = DLT_EN10MB RawPcapWriter._write_header(self, pkt) - def _write_packet(self, packet): - if isinstance(packet, tuple): - for pkt in packet: - self._write_packet(pkt) - return - sec = int(packet.time) - usec = int(round((packet.time - sec) * (1000000000 if self.nano else 1000000))) # noqa: E501 + def _write_packet(self, packet, sec=None, usec=None, caplen=None, + wirelen=None): + """ + Writes a single packet to the pcap file. + + :param packet: Packet, or bytes for a single packet + :type packet: Packet or bytes + :param sec: time the packet was captured, in seconds since epoch. If + not supplied, defaults to now. + :type sec: int or long + :param usec: If ``nano=True``, then number of nanoseconds after the + second that the packet was captured. If ``nano=False``, + then the number of microseconds after the second the + packet was captured. If ``sec`` is not specified, + this value is ignored. + :type usec: int or long + :param caplen: The length of the packet in the capture file. If not + specified, uses ``len(raw(packet))``. + :type caplen: int + :param wirelen: The length of the packet on the wire. If not + specified, tries ``packet.wirelen``, otherwise uses + ``caplen``. + :type wirelen: int + :returns: None + :rtype: None + """ + if hasattr(packet, "time"): + if sec is None: + sec = int(packet.time) + usec = int(round((packet.time - sec) * + (1000000000 if self.nano else 1000000))) + if usec is None: + usec = 0 + rawpkt = raw(packet) - caplen = len(rawpkt) - RawPcapWriter._write_packet(self, rawpkt, sec=sec, usec=usec, caplen=caplen, # noqa: E501 - wirelen=packet.wirelen or caplen) + caplen = len(rawpkt) if caplen is None else caplen + + if wirelen is None: + if hasattr(packet, "wirelen"): + wirelen = packet.wirelen + if wirelen is None: + wirelen = caplen + + RawPcapWriter._write_packet( + self, rawpkt, sec=sec, usec=usec, caplen=caplen, wirelen=wirelen) @conf.commands.register @@ -1365,24 +1441,54 @@ def import_hexcap(): @conf.commands.register -def wireshark(pktlist, **kwargs): - """Run wireshark on a list of packets""" - f = get_temp_file() - wrpcap(f, pktlist, **kwargs) - with ContextManagerSubprocess("wireshark()", conf.prog.wireshark): - subprocess.Popen([conf.prog.wireshark, "-r", f]) +def wireshark(pktlist, wait=False, **kwargs): + """ + Runs Wireshark on a list of packets. + + See :func:`tcpdump` for more parameter description. + + Note: this defaults to wait=False, to run Wireshark in the background. + """ + return tcpdump(pktlist, prog=conf.prog.wireshark, wait=wait, **kwargs) @conf.commands.register -def tdecode(pktlist): - """Run tshark -V on a list of packets""" - tcpdump(pktlist, prog=conf.prog.tshark, args=["-V"]) +def tdecode(pktlist, args=None, **kwargs): + """ + Run tshark on a list of packets. + + :param args: If not specified, defaults to ``tshark -V``. + + See :func:`tcpdump` for more parameters. + """ + if args is None: + args = ["-V"] + return tcpdump(pktlist, prog=conf.prog.tshark, args=args, **kwargs) @conf.commands.register def tcpdump(pktlist, dump=False, getfd=False, args=None, - prog=None, getproc=False, quiet=False): - """Run tcpdump or tshark on a list of packets + prog=None, getproc=False, quiet=False, use_tempfile=None, + read_stdin_opts=None, linktype=None, wait=True): + """Run tcpdump or tshark on a list of packets. + + When using ``tcpdump`` on OSX (``prog == conf.prog.tcpdump``), this uses a + temporary file to store the packets. This works around a bug in Apple's + version of ``tcpdump``: http://apple.stackexchange.com/questions/152682/ + + Otherwise, the packets are passed in stdin. + + This function can be explicitly enabled or disabled with the + ``use_tempfile`` parameter. + + When using ``wireshark``, it will be called with ``-ki -`` to start + immediately capturing packets from stdin. + + Otherwise, the command will be run with ``-r -`` (which is correct for + ``tcpdump`` and ``tshark``). + + This can be overridden with ``read_stdin_opts``. This has no effect when + ``use_tempfile=True``, or otherwise reading packets from a regular file. pktlist: a Packet instance, a PacketList instance or a list of Packet instances. Can also be a filename (as a string), an open @@ -1397,6 +1503,19 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, args=["-T", "json"]). prog: program to use (defaults to tcpdump, will work with tshark) quiet: when set to True, the process stderr is discarded +use_tempfile: When set to True, always use a temporary file to store packets. + When set to False, pipe packets through stdin. + When set to None (default), only use a temporary file with + ``tcpdump`` on OSX. +read_stdin_opts: When set, a list of arguments needed to capture from stdin. + Otherwise, attempts to guess. +linktype: If a Packet (or list) is passed in the ``pktlist`` argument, + set the ``linktype`` parameter on ``wrpcap``. If ``pktlist`` is a + path to a pcap file, then this option will have no effect. +wait: If True (default), waits for the process to terminate before returning + to Scapy. If False, the process will be detached to the background. If + dump, getproc or getfd is True, these have the same effect as + ``wait=False``. Examples: @@ -1430,18 +1549,42 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, u'_type': u'pcap_file'}] >>> json_data[0]['_source']['layers']['ip']['ip.ttl'] u'64' - """ getfd = getfd or getproc if prog is None: prog = [conf.prog.tcpdump] + _prog_name = "windump()" if WINDOWS else "tcpdump()" elif isinstance(prog, six.string_types): + _prog_name = "{}()".format(prog) prog = [prog] - _prog_name = "windump()" if WINDOWS else "tcpdump()" + else: + raise ValueError("prog must be a string") + # Build Popen arguments - args = args if args is not None else [] + if args is None: + args = [] + else: + # Make a copy of args + args = list(args) + stdout = subprocess.PIPE if dump or getfd else None stderr = open(os.devnull) if quiet else None + + if use_tempfile is None: + # Apple's tcpdump cannot read from stdin, see: + # http://apple.stackexchange.com/questions/152682/ + use_tempfile = DARWIN and prog[0] == conf.prog.tcpdump + + if read_stdin_opts is None: + if prog[0] == conf.prog.wireshark: + # Start capturing immediately (-k) from stdin (-i -) + read_stdin_opts = ["-ki", "-"] + else: + read_stdin_opts = ["-r", "-"] + else: + # Make a copy of read_stdin_opts + read_stdin_opts = list(read_stdin_opts) + if pktlist is None: # sniff with ContextManagerSubprocess(_prog_name, prog[0]): @@ -1458,14 +1601,12 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, stdout=stdout, stderr=stderr, ) - elif DARWIN: - # Tcpdump cannot read from stdin, see - # - tmpfile = tempfile.NamedTemporaryFile(delete=False) + elif use_tempfile: + tmpfile = get_temp_file(autoext=".pcap", fd=True) try: tmpfile.writelines(iter(lambda: pktlist.read(1048576), b"")) except AttributeError: - wrpcap(tmpfile, pktlist) + wrpcap(tmpfile, pktlist, linktype=linktype) else: tmpfile.close() with ContextManagerSubprocess(_prog_name, prog[0]): @@ -1474,12 +1615,11 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, stdout=stdout, stderr=stderr, ) - conf.temp_files.append(tmpfile.name) else: # pass the packet stream with ContextManagerSubprocess(_prog_name, prog[0]): proc = subprocess.Popen( - prog + ["-r", "-"] + args, + prog + read_stdin_opts + args, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, @@ -1487,7 +1627,7 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, try: proc.stdin.writelines(iter(lambda: pktlist.read(1048576), b"")) except AttributeError: - wrpcap(proc.stdin, pktlist) + wrpcap(proc.stdin, pktlist, linktype=linktype) except UnboundLocalError: raise IOError("%s died unexpectedly !" % prog) else: @@ -1498,7 +1638,8 @@ def tcpdump(pktlist, dump=False, getfd=False, args=None, return proc if getfd: return proc.stdout - proc.wait() + if wait: + proc.wait() @conf.commands.register diff --git a/test/pipetool.uts b/test/pipetool.uts index 2b1f526c5f3..0f7d3da42d7 100644 --- a/test/pipetool.uts +++ b/test/pipetool.uts @@ -27,11 +27,6 @@ time.sleep(3) s.msg = [] p.stop() -try: - os.remove("test.png") -except OSError: - pass - = Test add_pipe s = AutoSource() @@ -170,6 +165,7 @@ p.start() p.wait_and_stop() assert c.recv() == "hello" +assert c.recv(block=False) is None = Test UpDrain @@ -237,18 +233,61 @@ mocked_l2listen.start() try: p = PipeEngine() s = SniffSource() + assert s.s is None d1 = Drain(name="d1") c = QueueSink(name="c") s > d1 > c p.add(s) p.start() assert c.q.get(2) + assert s.s is not None p.stop() finally: mocked_l2listen.stop() os.close(r) os.close(w) += Test SniffSource with socket + +r, w = os.pipe() +os.write(w, b"X") + +class FakeSocket(object): + def __init__(self): + self.times = 0 + def recv(self, x=None): + if self.times > 2: + return + self.times += 1 + return Raw(b'hello') + def fileno(self): + return r + +try: + p = PipeEngine() + s = SniffSource(socket=FakeSocket()) + assert s.s is not None + d = Drain() + c = QueueSink() + p.add(s > d > c) + p.start() + msg = c.q.get(timeout=1) + p.stop() + assert raw(msg) == b'hello' +finally: + os.close(r) + os.close(w) + += Test SniffSource with invalid args + +try: + s = SniffSource(iface='eth0', socket='not a socket') +except ValueError: + pass +else: + # expected ValueError + assert False + = Test exhausted AutoSource and SniffSource import mock @@ -273,31 +312,110 @@ try: except: pass += Test WiresharkSink + +from io import BytesIO + +f = BytesIO() +pkt = Ether()/IP()/ICMP() + +with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: + p = PipeEngine() + src = CLIFeeder() + sink = WiresharkSink() + p.add(src > sink) + p.start() + src.send(pkt) + time.sleep(3) + # Prevent stop from closing the BytesIO + with mock.patch.object(f, 'close'): + p.stop() + +popen.assert_called_once_with( + [conf.prog.wireshark, '-ki', '-'], stdin=subprocess.PIPE, stdout=None, + stderr=None) +bytes_hex(f.getvalue()) +bytes_hex(raw(pkt)) +assert raw(pkt) in f.getvalue() + += Test WiresharkSink with linktype + +f = BytesIO() +pkt = Ether()/IP()/ICMP() +linktype = scapy.data.DLT_EN3MB + +with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: + p = PipeEngine() + src = CLIFeeder() + sink = WiresharkSink(linktype=linktype) + p.add(src > sink) + p.start() + src.send(pkt) + time.sleep(3) + # Prevent stop from closing the BytesIO + with mock.patch.object(f, 'close'): + p.stop() + +popen.assert_called_once_with( + [conf.prog.wireshark, '-ki', '-'], + stdin=subprocess.PIPE, stdout=None, stderr=None) + +bytes_hex(f.getvalue()) +bytes_hex(raw(pkt)) +assert raw(pkt) in f.getvalue() + +# Check that the linktype was also correct +f.seek(0) or None +r = PcapReader(f) +assert r.linktype == DLT_EN3MB + += Test WiresharkSink with args + +f = BytesIO() +pkt = Ether()/IP()/ICMP() + +with mock.patch("subprocess.Popen", return_value=Bunch(stdin=f)) as popen: + p = PipeEngine() + src = CLIFeeder() + sink = WiresharkSink(args=['-c', '1']) + p.add(src > sink) + p.start() + src.send(pkt) + time.sleep(3) + # Prevent stop from closing the BytesIO + with mock.patch.object(f, 'close'): + p.stop() + +popen.assert_called_once_with( + [conf.prog.wireshark, '-ki', '-', '-c', '1'], + stdin=subprocess.PIPE, stdout=None, stderr=None) + = Test RdpcapSource and WrpcapSink -~ needs_root + +dname = get_temp_dir() req = Ether()/IP()/ICMP() rpy = Ether()/IP('E\x00\x00\x1c\x00\x00\x00\x004\x01\x1d\x04\xd8:\xd0\x83\xc0\xa8\x00w\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -wrpcap("t.pcap", [req, rpy]) +wrpcap(os.path.join(dname, "t.pcap"), [req, rpy]) p = PipeEngine() -s = RdpcapSource("t.pcap") +s = RdpcapSource(os.path.join(dname, "t.pcap")) d1 = Drain(name="d1") -c = WrpcapSink("t2.pcap", name="c") +c = WrpcapSink(os.path.join(dname, "t2.pcap"), name="c") s > d1 > c p.add(s) p.start() p.wait_and_stop() -results = rdpcap("t2.pcap") +results = rdpcap(os.path.join(dname, "t2.pcap")) assert raw(results[0]) == raw(req) assert raw(results[1]) == raw(rpy) -os.unlink("t.pcap") -os.unlink("t2.pcap") +os.unlink(os.path.join(dname, "t.pcap")) +os.unlink(os.path.join(dname, "t2.pcap")) = Test InjectSink and Inject3Sink ~ needs_root diff --git a/test/regression.uts b/test/regression.uts index affde2a4cca..14d1e3f1485 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -6691,6 +6691,41 @@ assert isinstance(pkt, Padding) and pkt.load == b'\xeay$\xf6' pkt = pkt.payload assert isinstance(pkt, NoPayload) += Check PcapWriter on null write + +f = BytesIO() +w = PcapWriter(f) +w.write([]) +assert len(f.getvalue()) == 0 + +# Stop being closed for reals, but we still want to have the header written +with mock.patch.object(f, 'close') as cf: + w.close() + +cf.assert_called_once_with() +assert len(f.getvalue()) != 0 + += Check PcapWriter sets correct linktype after null write + +f = BytesIO() +w = PcapWriter(f) +w.write([]) +assert len(f.getvalue()) == 0 +w.write(Ether()/IP()/ICMP()) +assert len(f.getvalue()) != 0 + +# Stop being closed for reals, but we still want to have the header written +with mock.patch.object(f, 'close') as cf: + w.close() + +cf.assert_called_once_with() +f.seek(0) or None +assert len(f.getvalue()) != 0 + +r = PcapReader(f) +assert r.LLcls is Ether +assert r.linktype == DLT_EN10MB + = Check tcpdump() ~ tcpdump * No very specific tests because we do not want to depend on tcpdump output @@ -6701,16 +6736,154 @@ assert b'IP 127.0.0.1.20 > 127.0.0.1.80:' in data[0] assert b'IP 127.0.0.1.53 > 127.0.0.1.53:' in data[1] assert b'IP 127.0.0.1 > 127.0.0.1:' in data[2] +# Also check with use_tempfile=True (for non-OSX platforms) +pcapfile.seek(0) or None +tempfile_count = len(conf.temp_files) +data = tcpdump(pcapfile, dump=True, args=['-nn'], use_tempfile=True).split(b'\n') +print(data) +assert b'IP 127.0.0.1.20 > 127.0.0.1.80:' in data[0] +assert b'IP 127.0.0.1.53 > 127.0.0.1.53:' in data[1] +assert b'IP 127.0.0.1 > 127.0.0.1:' in data[2] +# We should have another tempfile tracked. +assert len(conf.temp_files) > tempfile_count + +# Check with a simple packet +data = tcpdump([Ether()/IP()/ICMP()], dump=True, args=['-nn']).split(b'\n') +print(data) +assert b'IP 127.0.0.1 > 127.0.0.1: ICMP' in data[0] + += Check tcpdump() command with linktype + +f = BytesIO() +pkt = Ether()/IP()/ICMP() + +with mock.patch('subprocess.Popen', return_value=Bunch( + stdin=f, wait=lambda: None)) as popen: + # Prevent closing the BytesIO + with mock.patch.object(f, 'close'): + tcpdump([pkt], linktype=scapy.data.DLT_EN3MB, use_tempfile=False) + +popen.assert_called_once_with( + [conf.prog.tcpdump, '-r', '-'], + stdin=subprocess.PIPE, stdout=None, stderr=None) + +print(bytes_hex(f.getvalue())) +assert raw(pkt) in f.getvalue() +f.close() +del f, pkt + += Check tcpdump() command with linktype and args + +f = BytesIO() +pkt = Ether()/IP()/ICMP() + +with mock.patch('subprocess.Popen', return_value=Bunch( + stdin=f, wait=lambda: None)) as popen: + # Prevent closing the BytesIO + with mock.patch.object(f, 'close'): + tcpdump([pkt], linktype=scapy.data.DLT_EN3MB, use_tempfile=False, + args=['-y', 'DLT_EN10MB']) + +popen.assert_called_once_with( + [conf.prog.tcpdump, '-r', '-', '-y', 'DLT_EN10MB'], + stdin=subprocess.PIPE, stdout=None, stderr=None) + +print(bytes_hex(f.getvalue())) +assert raw(pkt) in f.getvalue() +f.close() +del f, pkt + += Check tcpdump() command rejects non-string input for prog + +pkt = Ether()/IP()/ICMP() + +try: + tcpdump([pkt], prog=+17607067425, args=['-nn']) +except ValueError as e: + if hasattr(e, 'args'): + assert 'prog' in e.args[0] + else: + assert 'prog' in e.message +else: + assert False, 'expected exception' + = Check tcpdump() command with tshark ~ tshark pcapfile = BytesIO(b'\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00e\x00\x00\x00\xcf\xc5\xacVo*\n\x00(\x00\x00\x00(\x00\x00\x00E\x00\x00(\x00\x01\x00\x00@\x06|\xcd\x7f\x00\x00\x01\x7f\x00\x00\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x91|\x00\x00\xcf\xc5\xacV_-\n\x00\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x11|\xce\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00\x08\x01r\xcf\xc5\xacV\xf90\n\x00\x1c\x00\x00\x00\x1c\x00\x00\x00E\x00\x00\x1c\x00\x01\x00\x00@\x01|\xde\x7f\x00\x00\x01\x7f\x00\x00\x01\x08\x00\xf7\xff\x00\x00\x00\x00') +# tshark doesn't need workarounds on OSX +tempfile_count = len(conf.temp_files) values = [tuple(int(val) for val in line[:-1].split(b'\t')) for line in tcpdump(pcapfile, prog=conf.prog.tshark, getfd=True, args=['-T', 'fields', '-e', 'ip.ttl', '-e', 'ip.proto'])] assert values == [(64, 6), (64, 17), (64, 1)] +assert len(conf.temp_files) == tempfile_count + += Check tdecode command directly for tshark +~ tshark + +pkts = [ + Ether()/IP(src='192.0.2.1', dst='192.0.2.2')/ICMP(type='echo-request')/Raw(b'X'*100), + Ether()/IP(src='192.0.2.2', dst='192.0.2.1')/ICMP(type='echo-reply')/Raw(b'X'*100), +] + +# tshark doesn't need workarounds on OSX +tempfile_count = len(conf.temp_files) + +r = tdecode(pkts, dump=True) +r +assert b'Src: 192.0.2.1' in r +assert b'Src: 192.0.2.2' in r +assert b'Dst: 192.0.2.2' in r +assert b'Dst: 192.0.2.1' in r +assert b'Echo (ping) request' in r +assert b'Echo (ping) reply' in r +assert b'ICMP' in r +assert len(conf.temp_files) == tempfile_count + += Check tdecode with linktype +~ tshark + +# These are the same as the ping packets above +pkts = [ + b'\xff\xff\xff\xff\xff\xff\xac"\x0b\xc5j\xdb\x08\x00E\x00\x00\x80\x00\x01\x00\x00@\x01\xf6x\xc0\x00\x02\x01\xc0\x00\x02\x02\x08\x00\xb6\xbe\x00\x00\x00\x00XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + b'\xff\xff\xff\xff\xff\xff\xac"\x0b\xc5j\xdb\x08\x00E\x00\x00\x80\x00\x01\x00\x00@\x01\xf6x\xc0\x00\x02\x02\xc0\x00\x02\x01\x00\x00\xbe\xbe\x00\x00\x00\x00XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', +] + +# tshark doesn't need workarounds on OSX +tempfile_count = len(conf.temp_files) + +r = tdecode(pkts, dump=True, linktype=DLT_EN10MB) +assert b'Src: 192.0.2.1' in r +assert b'Src: 192.0.2.2' in r +assert b'Dst: 192.0.2.2' in r +assert b'Dst: 192.0.2.1' in r +assert b'Echo (ping) request' in r +assert b'Echo (ping) reply' in r +assert b'ICMP' in r +assert len(conf.temp_files) == tempfile_count + = Run scapy's tshark command ~ netaccess tshark(count=1, timeout=3) += Check wireshark() + +f = BytesIO() +pkt = Ether()/IP()/ICMP() + +with mock.patch('subprocess.Popen', return_value=Bunch(stdin=f)) as popen: + # Prevent closing the BytesIO + with mock.patch.object(f, 'close'): + wireshark([pkt]) + +popen.assert_called_once_with( + [conf.prog.wireshark, '-ki', '-'], + stdin=subprocess.PIPE, stdout=None, stderr=None) + +print(bytes_hex(f.getvalue())) +assert raw(pkt) in f.getvalue() +f.close() +del f, pkt + = Check Raw IP pcap files import tempfile @@ -11832,3 +12005,8 @@ os.remove(filename_css) # assert(output == expected) # #test_snmpwalk("secdev.org") + += test get_temp_dir + +dname = get_temp_dir() +assert os.path.isdir(dname)