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