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 <acon...@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@openvswitch.org > http://openvswitch.org/mailman/listinfo/dev _______________________________________________ dev mailing list dev@openvswitch.org http://openvswitch.org/mailman/listinfo/dev