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

Flavio Leitner fbl at sysclose.org
Tue May 24 22:32:24 UTC 2016


On Tue, May 24, 2016 at 04:35:29PM -0400, Aaron Conole wrote:
> 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).

This is great and helps in the usability front, specially when
one is using userspace datapath only.

Overall it looks good but I haven't really looked at the OVSDB 
calls yet.  Some other comments inline.


> 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

Should it be @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)

This would work fine for packaged installations, but if one
installs on /usr/local, I think the default PYTHONPATH doesn't
cover that.  I am not a python expert, but maybe it could
mention about PYTHONPATH?  Just a suggestion.


> +
> +
> +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'

On my system this is:
# Check whether --with-rundir was given.
if test "${with_rundir+set}" = set; then :
  withval=$with_rundir; RUNDIR=$withval
else
  RUNDIR='${localstatedir}/run/openvswitch'
fi

Note that /openvswitch is already there, so by default the
--db-sock ends up being in
unix:/usr/local/var/run/openvswitch/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)

Maybe print that only when debug is enabled?


> +
> +    ovsdb = OVSDB(db_sock)
> +    if mirror_interface is None:

mirror_interface is referenced but it hasn't been initialized.


> +        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)

It could create an internal port here and probably abstract that?

> +
> +    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)

Do we need the sleep above?

> +    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."

This could be more helpful and show the interface and mirrors names
at least.

Thanks Aaron!
fbl

> +        sys.exit(1)
> +    sys.exit(0)
> +
> +
> +if __name__ == '__main__':
> +    main()
> +
> +# Local variables:
> +# mode: python
> +# End:
> -- 
> 2.5.5
> 
> _______________________________________________
> dev mailing list
> dev at openvswitch.org
> http://openvswitch.org/mailman/listinfo/dev





More information about the dev mailing list