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