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

Aaron Conole aconole at redhat.com
Wed May 25 15:08:37 UTC 2016


Flavio Leitner <fbl at sysclose.org> writes:

> 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@ ?

d'oh! I had this originally, and changed it to test on a lab machine. It
will be fixed.

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

Will do.

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

d'oh! Okay - I will validate the string.

>> +""" % {'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?

Good call.  I will do that.

>> +
>> +    ovsdb = OVSDB(db_sock)
>> +    if mirror_interface is None:
>
> mirror_interface is referenced but it hasn't been initialized.

Python :-)  Technically, it's allowed, and flake8 or pyflakes doesn't
complain (because it is perfectly valid python).  However, I can
rewrite it something like

mirror_interface = None
...
mirror_interface = mirror_interface or "mi_%s" % interface

if you think that feels more natural. Doesn't matter to me.

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

I thought about it, but I don't know the right way of doing that on
anything but linux (and especially don't even have a windows machine to
try it on).  That said, I'll see what I can cook up.

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

I put it in originally when I was debugging something.  I will remove it.

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

Agreed.

> Thanks Aaron!

No!  Thank you for the review, Flavio!

> 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