[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