[ovs-dev] [RFC] ovs-tcpdump: Add a tcpdump wrapper utility

Aaron Conole aconole at redhat.com
Tue May 24 20:35:29 UTC 2016


Currently, there is some documentation which describes setting up and
using port mirrors for bridges. This documentation is helpful to setup
a packet capture for specific ports.

However, a utility to do such packet capture would be valuable, both
as an exercise in documenting the steps an additional time, and as a way
of providing an out-of-the-box experience for running a capture.

This commit adds a tcpdump-wrapper utility for such purpose. It uses the
Open vSwitch python library to add/remove ports and mirrors to/from the
Open vSwitch database. It will create a tcpdump instance listening on
the mirror port (allowing the user to specify additional arguments), and
dump data to the screen (or otherwise).

Signed-off-by: Aaron Conole <aconole at redhat.com>
---
 NEWS                       |   2 +
 utilities/automake.mk      |   5 +
 utilities/ovs-tcpdump.8.in |  38 +++++
 utilities/ovs-tcpdump.in   | 398 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 443 insertions(+)
 create mode 100644 utilities/ovs-tcpdump.8.in
 create mode 100755 utilities/ovs-tcpdump.in

diff --git a/NEWS b/NEWS
index 4e81cad..a32350c 100644
--- a/NEWS
+++ b/NEWS
@@ -54,6 +54,8 @@ Post-v2.5.0
      * Flow based tunnel match and action can be used for IPv6 address using
        tun_ipv6_src, tun_ipv6_dst fields.
      * Added support for IPv6 tunnels to native tunneling.
+   - A wrapper script, 'ovs-tcpdump', to easily port-mirror an OVS port and
+     watch with tcpdump
 
 v2.5.0 - 26 Feb 2016
 ---------------------
diff --git a/utilities/automake.mk b/utilities/automake.mk
index 1cc66b6..f236ec4 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -12,6 +12,7 @@ bin_SCRIPTS += \
 	utilities/ovs-l3ping \
 	utilities/ovs-parse-backtrace \
 	utilities/ovs-pcap \
+	utilities/ovs-tcpdump \
 	utilities/ovs-tcpundump \
 	utilities/ovs-test \
 	utilities/ovs-vlan-test
@@ -52,6 +53,7 @@ EXTRA_DIST += \
 	utilities/ovs-pipegen.py \
 	utilities/ovs-pki.in \
 	utilities/ovs-save \
+	utilities/ovs-tcpdump.in \
 	utilities/ovs-tcpundump.in \
 	utilities/ovs-test.in \
 	utilities/ovs-vlan-test.in \
@@ -69,6 +71,7 @@ MAN_ROOTS += \
 	utilities/ovs-parse-backtrace.8 \
 	utilities/ovs-pcap.1.in \
 	utilities/ovs-pki.8.in \
+	utilities/ovs-tcpdump.8.in \
 	utilities/ovs-tcpundump.1.in \
 	utilities/ovs-vlan-bug-workaround.8.in \
 	utilities/ovs-test.8.in \
@@ -94,6 +97,8 @@ DISTCLEANFILES += \
 	utilities/ovs-pki.8 \
 	utilities/ovs-sim \
 	utilities/ovs-sim.1 \
+	utilities/ovs-tcpdump \
+	utilities/ovs-tcpdump.8 \
 	utilities/ovs-tcpundump \
 	utilities/ovs-tcpundump.1 \
 	utilities/ovs-test \
diff --git a/utilities/ovs-tcpdump.8.in b/utilities/ovs-tcpdump.8.in
new file mode 100644
index 0000000..044e053
--- /dev/null
+++ b/utilities/ovs-tcpdump.8.in
@@ -0,0 +1,38 @@
+.TH ovs\-tcpdump 8 "@VERSION@" "Open vSwitch" "Open vSwitch Manual"
+.
+.SH NAME
+ovs\-tcpdump \- Dump traffic from an Open vSwitch port using \fBtcpdump\fR.
+.
+.SH SYNOPSIS
+\fBovs\-tcpdump\fR \fB\-i\fR \fIport\fR \fBtcpdump options...\fR
+.
+.SH DESCRIPTION
+\fBovs\-tcpdump\fR creates switch mirror ports in the \fBovs\-vswitchd\fR
+daemon and executes \fBtcpdump\fR to listen against those ports. When the
+\fBtcpdump\fR instance exits, it then cleans up the mirror port it created.
+.PP
+\fBovs\-tcpdump\fR will not allow multiple mirrors for the same port. It has
+some logic to parse the current configuration and prevent duplicate mirrors.
+.PP
+The \fB\-i\fR option may not appear multiple times.
+.
+.SH "OPTIONS"
+.so lib/common.man
+.
+.IP "\fB\-i\fR"
+.IQ "\fB\-\-interface\fR"
+The interface for which a mirror port should be created, and packets should
+be dumped.
+.
+.IP "\fB\-\-db\-sock\fR"
+The Open vSwitch database socket connection string. The default is
+\fIunix:@RUNDIR@/openvswitch/db.sock\fR
+.
+.SH "SEE ALSO"
+.
+.BR ovs\-appctl (8),
+.BR ovs\-vswitchd (8),
+.BR ovs\-pcap (1),
+.BR ovs\-tcpundump (1),
+.BR tcpdump (8),
+.BR wireshark (8).
diff --git a/utilities/ovs-tcpdump.in b/utilities/ovs-tcpdump.in
new file mode 100755
index 0000000..b1cd652
--- /dev/null
+++ b/utilities/ovs-tcpdump.in
@@ -0,0 +1,398 @@
+#! /usr/bin/env /usr/bin/python
+#
+# Copyright (c) 2016 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import sys
+import time
+import netifaces
+import os
+import pwd
+
+try:
+    from ovs.stream import Stream
+    from ovs.db import idl
+    from ovs.poller import Poller
+    from ovs import jsonrpc
+except:
+    print "ERROR: Please install the correct Open vSwitch python support"
+    print "       libraries (version @VERSION@)."
+    sys.exit(1)
+
+
+def _doexec(*args, **kwargs):
+    """Executes an application and returns a set of pipes to be used to
+       perform io"""
+    shell = len(args) == 1
+    proc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell,
+                            bufsize=0)
+    return proc
+
+
+def username():
+    return pwd.getpwuid(os.getuid())[0]
+
+
+def usage():
+    print """\
+%(prog)s: Open vSwitch tcpdump helper.
+usage: %(prog)s -i interface [TCPDUMP OPTIONS]
+where TCPDUMP OPTIONS represents the options normally passed to tcpdump.
+
+The following options are available:
+   -h, --help                 display this help message
+   -V, --version              display version information
+   -i, --interface            Open vSwitch interface to mirror and tcpdump
+   --mirror-to                The name for the mirror port to use (optional)
+                              Default 'mi_INTERFACE'
+   --db-sock                  A connection string to reach the Open vSwitch
+                              ovsdb-server.
+                              Default 'unix:@RUNDIR@/openvswitch/db.sock'
+""" % {'prog': sys.argv[0]}
+    sys.exit(0)
+
+
+class OVSDBException(Exception):
+    pass
+
+
+class OVSDB(object):
+    @staticmethod
+    def wait_for_db_change(idl):
+        seq = idl.change_seqno
+        stop = time.time() + 10
+        while idl.change_seqno == seq and not idl.run():
+            poller = Poller()
+            idl.wait(poller)
+            poller.block()
+            if time.time() >= stop:
+                raise Exception('Retry Timeout')
+
+    def __init__(self, db_sock):
+        self._db_sock = db_sock
+        self._txn = None
+        schema = self._get_schema()
+        schema.register_all()
+        self._idl_conn = idl.Idl(db_sock, schema)
+        OVSDB.wait_for_db_change(self._idl_conn)  # Initial Sync with DB
+
+    def _get_schema(self):
+        error, strm = Stream.open_block(Stream.open(self._db_sock))
+        if error:
+            raise Exception("Unable to connect to %s" % self._db_sock)
+        rpc = jsonrpc.Connection(strm)
+        req = jsonrpc.Message.create_request('get_schema', ['Open_vSwitch'])
+        error, resp = rpc.transact_block(req)
+        rpc.close()
+
+        if error or resp.error:
+            raise Exception('Unable to retrieve schema.')
+        return idl.SchemaHelper(None, resp.result)
+
+    def get_table(self, table_name):
+        return self._idl_conn.tables[table_name]
+
+    def _start_txn(self):
+        if self._txn is not None:
+            raise OVSDBException("ERROR: A transaction was started already")
+        self._idl_conn.change_seqno += 1
+        self._txn = idl.Transaction(self._idl_conn)
+        return self._txn
+
+    def _complete_txn(self, try_again_fn):
+        if self._txn is None:
+            raise OVSDBException("ERROR: Not in a transaction")
+        status = self._txn.commit_block()
+        if status is idl.Transaction.TRY_AGAIN:
+            if self._idl_conn._session.rpc.status != 0:
+                self._idl_conn.force_reconnect()
+                OVSDB.wait_for_db_change(self._idl_conn)
+            return try_again_fn(self)
+        elif status is idl.Transaction.ERROR:
+            return False
+
+    def _find_row(self, table_name, find):
+        return next(
+            (row for row in self.get_table(table_name).rows.values()
+             if find(row)), None)
+
+    def _find_row_by_name(self, table_name, value):
+        return self._find_row(table_name, lambda row: row.name == value)
+
+    def port_exists(self, port_name):
+        return bool(self._find_row_by_name('Port', port_name))
+
+    def port_bridge(self, port_name):
+        try:
+            row = self._find_row_by_name('Interface', port_name)
+            port = self._find_row('Port', lambda x: row in x.interfaces)
+            br = self._find_row('Bridge', lambda x: port in x.ports)
+            return br.name
+        except:
+            raise OVSDBException('Unable to find port %s bridge' % port_name)
+
+    def interface_exists(self, intf_name):
+        return bool(self._find_row_by_name('Interface', intf_name))
+
+    def mirror_exists(self, mirror_name):
+        return bool(self._find_row_by_name('Mirror', mirror_name))
+
+    def interface_uuid(self, intf_name):
+        row = self._find_row_by_name('Interface', intf_name)
+        if bool(row):
+            return row.uuid
+        raise OVSDBException('No such interface: %s' % intf_name)
+
+    def make_interface(self, intf_name, execute_transaction=True):
+        if self.interface_exists(intf_name):
+            print "INFO: Interface exists."
+            return self.interface_uuid(intf_name)
+
+        txn = self._start_txn()
+        tmp_row = txn.insert(self.get_table('Interface'))
+        tmp_row.name = intf_name
+
+        def try_again(db_entity):
+            db_entity.make_interface(intf_name)
+
+        if not execute_transaction:
+            return tmp_row
+
+        txn.add_comment("ovs-tcpdump: user=%s,create_intf=%s"
+                        % (username(), intf_name))
+        status = self._complete_txn(try_again)
+        if status is False:
+            raise OVSDBException('Unable to create Interface %s' % intf_name)
+        result = txn.get_insert_uuid(tmp_row.uuid)
+        self._txn = None
+        return result
+
+    def destroy_port(self, port_name, bridge_name):
+        if not self.interface_exists(port_name):
+            return
+        txn = self._start_txn()
+        br = self._find_row_by_name('Bridge', bridge_name)
+        ports = [port for port in br.ports if port.name != port_name]
+        br.ports = ports
+
+        def try_again(db_entity):
+            db_entity.destroy_port(port_name)
+
+        txn.add_comment("ovs-tcpdump: user=%s,destroy_port=%s"
+                        % (username(), port_name))
+        status = self._complete_txn(try_again)
+        if status is False:
+            raise OVSDBException('unable to delete Port %s' % port_name)
+        self._txn = None
+
+    def destroy_mirror(self, mirror_name, bridge_name):
+        if not self.mirror_exists(mirror_name):
+            return
+        txn = self._start_txn()
+        mirror_row = self._find_row_by_name('Mirror', mirror_name)
+        br = self._find_row_by_name('Bridge', bridge_name)
+        mirrors = [mirror for mirror in br.mirrors
+                   if mirror.uuid != mirror_row.uuid]
+        br.mirrors = mirrors
+
+        def try_again(db_entity):
+            db_entity.destroy_mirror(mirror_name, bridge_name)
+
+        txn.add_comment("ovs-tcpdump: user=%s,destroy_mirror=%s"
+                        % (username(), mirror_name))
+        status = self._complete_txn(try_again)
+        if status is False:
+            print "NO: %s" % txn.get_error()
+            raise OVSDBException('Unable to delete Mirror %s' % mirror_name)
+        self._txn = None
+
+    def make_port(self, port_name, bridge_name):
+        iface_row = self.make_interface(port_name, False)
+        txn = self._txn
+
+        br = self._find_row_by_name('Bridge', bridge_name)
+        if not br:
+            raise OVSDBException('Bad bridge name %s' % bridge_name)
+
+        port = txn.insert(self.get_table('Port'))
+        port.name = port_name
+
+        br.verify('ports')
+        ports = getattr(br, 'ports', [])
+        ports.append(port)
+        br.ports = ports
+
+        port.verify('interfaces')
+        ifaces = getattr(port, 'interfaces', [])
+        ifaces.append(iface_row)
+        port.interfaces = ifaces
+
+        def try_again(db_entity):
+            db_entity.make_port(port_name, bridge_name)
+
+        txn.add_comment("ovs-tcpdump: user=%s,create_port=%s"
+                        % (username(), port_name))
+        status = self._complete_txn(try_again)
+        if status is False:
+            raise OVSDBException('Unable to create Port %s: %s' %
+                                 (port_name, txn.get_error()))
+        result = txn.get_insert_uuid(port.uuid)
+        self._txn = None
+        return result
+
+    def bridge_mirror(self, intf_name, mirror_intf_name, br_name):
+
+        txn = self._start_txn()
+        mirror = txn.insert(self.get_table('Mirror'))
+        mirror.name = 'm_%s' % intf_name
+
+        mirror.select_all = False
+
+        mirrored_port = self._find_row_by_name('Port', intf_name)
+
+        mirror.verify('select_dst_port')
+        dst_port = getattr(mirror, 'select_dst_port', [])
+        dst_port.append(mirrored_port)
+        mirror.select_dst_port = dst_port
+
+        mirror.verify('select_src_port')
+        src_port = getattr(mirror, 'select_src_port', [])
+        src_port.append(mirrored_port)
+        mirror.select_src_port = src_port
+
+        output_port = self._find_row_by_name('Port', mirror_intf_name)
+
+        mirror.verify('output_port')
+        out_port = getattr(mirror, 'output_port', [])
+        out_port.append(output_port.uuid)
+        mirror.output_port = out_port
+
+        br = self._find_row_by_name('Bridge', br_name)
+        br.verify('mirrors')
+        mirrors = getattr(br, 'mirrors', [])
+        mirrors.append(mirror.uuid)
+        br.mirrors = mirrors
+
+        def try_again(db_entity):
+            db_entity.bridge_mirror(intf_name, mirror_intf_name, br_name)
+
+        txn.add_comment("ovs-tcpdump: user=%s,create_mirror=%s"
+                        % (username(), mirror.name))
+        status = self._complete_txn(try_again)
+        if status is False:
+            print "NO: %s" % txn.get_error()
+            raise OVSDBException('Unable to create Mirror %s: %s' %
+                                 (mirror_intf_name, txn.get_error()))
+        result = txn.get_insert_uuid(mirror.uuid)
+        self._txn = None
+        return result
+
+
+def argv_tuples(lst):
+    cur, nxt = iter(lst), iter(lst)
+    next(nxt, None)
+
+    try:
+        while True:
+            yield next(cur), next(nxt, None)
+    except StopIteration:
+        pass
+
+
+def main():
+    db_sock = 'unix:@RUNDIR@/openvswitch/db.sock'
+    interface = None
+    tcpdargs = []
+
+    skip_next = False
+    for cur, nxt in argv_tuples(sys.argv[1:]):
+        if skip_next:
+            skip_next = False
+            continue
+
+        if cur in ['-h', '--help']:
+            usage()
+        elif cur in ['-V', '--version']:
+            print "ovs-tcpdump (Open vSwitch) @VERSION@"
+            sys.exit(0)
+        elif cur in ['--mirror-to']:
+            mirror_interface = nxt
+            skip_next = True
+        elif cur in ['--db-sock']:
+            db_sock = nxt
+            skip_next = True
+            continue
+        elif cur in ['-i']:
+            interface = nxt
+            skip_next = True
+            continue
+        tcpdargs.append(cur)
+
+    if interface is None:
+        print "Error: must at least specify an interface with '-i' option"
+        sys.exit(1)
+
+    if '-l' not in tcpdargs:
+        tcpdargs.insert(0, '-l')
+
+    print "TCPDUMP Args: %s" % ' '.join(tcpdargs)
+
+    ovsdb = OVSDB(db_sock)
+    if mirror_interface is None:
+        mirror_interface = "mi_%s" % interface
+    if mirror_interface not in netifaces.interfaces():
+        print "ERROR: Please create a tap interface called `%s`" % \
+            mirror_interface
+        print "See your OS guide for how to do this."
+        print "Ex: ip tuntap add dev %s mode tap" % mirror_interface
+        sys.exit(1)
+
+    if not ovsdb.port_exists(interface):
+        print "ERROR: Port %s does not exist." % interface
+        sys.exit(1)
+    if ovsdb.port_exists(mirror_interface):
+        print "ERROR: Mirror port (%s) exists for port %s." % \
+              (mirror_interface, interface)
+        sys.exit(1)
+    try:
+        ovsdb.make_port(mirror_interface, ovsdb.port_bridge(interface))
+        ovsdb.bridge_mirror(interface, mirror_interface,
+                            ovsdb.port_bridge(interface))
+    except OVSDBException as oe:
+        print "ERROR: Unable to properly setup the mirror: %s." % str(oe)
+        sys.exit(1)
+
+    time.sleep(1)
+    pipes = _doexec(*(['tcpdump', '-i', mirror_interface] + tcpdargs))
+    try:
+        while True:
+            print pipes.stdout.readline()
+    except KeyboardInterrupt:
+        pipes.terminate()
+        ovsdb.destroy_mirror('m_%s' % interface, ovsdb.port_bridge(interface))
+        ovsdb.destroy_port(mirror_interface, ovsdb.port_bridge(interface))
+    except:
+        print "Unable to tear down the create ports and mirrors."
+        print "Please use ovs-vsctl to remove the ports and mirrors created."
+        sys.exit(1)
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
+
+# Local variables:
+# mode: python
+# End:
-- 
2.5.5




More information about the dev mailing list