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> --- v1->v2: * Added .gitignore entries * Fixed a manpages distribution error NEWS | 2 + manpages.mk | 6 + utilities/.gitignore | 2 + utilities/automake.mk | 9 +- utilities/ovs-tcpdump.8.in | 51 +++++ utilities/ovs-tcpdump.in | 453 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 utilities/ovs-tcpdump.8.in create mode 100755 utilities/ovs-tcpdump.in diff --git a/NEWS b/NEWS index ba201cf..042b361 100644 --- a/NEWS +++ b/NEWS @@ -55,6 +55,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/manpages.mk b/manpages.mk index d4d4f21..cf83e44 100644 --- a/manpages.mk +++ b/manpages.mk @@ -164,6 +164,12 @@ utilities/ovs-pki.8: \ utilities/ovs-pki.8.in utilities/ovs-pki.8.in: +utilities/ovs-tcpdump.8: \ + utilities/ovs-tcpdump.8.in \ + lib/common.man +utilities/ovs-tcpdump.8.in: +lib/common.man: + utilities/ovs-tcpundump.1: \ utilities/ovs-tcpundump.1.in \ lib/common-syn.man \ diff --git a/utilities/.gitignore b/utilities/.gitignore index 10cb4af..34c58f2 100644 --- a/utilities/.gitignore +++ b/utilities/.gitignore @@ -26,6 +26,8 @@ /ovs-pki.8 /ovs-test /ovs-test.8 +/ovs-tcpdump +/ovs-tcpdump.8 /ovs-tcpundump /ovs-tcpundump.1 /ovs-vlan-bug-workaround diff --git a/utilities/automake.mk b/utilities/automake.mk index 1cc66b6..9d5b425 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 \ @@ -114,6 +119,7 @@ man_MANS += \ utilities/ovs-parse-backtrace.8 \ utilities/ovs-pcap.1 \ utilities/ovs-pki.8 \ + utilities/ovs-tcpdump.8 \ utilities/ovs-tcpundump.1 \ utilities/ovs-vlan-bug-workaround.8 \ utilities/ovs-test.8 \ @@ -148,6 +154,7 @@ utilities_nlmon_LDADD = lib/libopenvswitch.la endif FLAKE8_PYFILES += utilities/ovs-pcap.in \ - utilities/checkpatch.py utilities/ovs-dev.py + utilities/checkpatch.py utilities/ovs-dev.py \ + utilities/ovs-tcpdump.in include utilities/bugtool/automake.mk diff --git a/utilities/ovs-tcpdump.8.in b/utilities/ovs-tcpdump.8.in new file mode 100644 index 0000000..ecd0937 --- /dev/null +++ b/utilities/ovs-tcpdump.8.in @@ -0,0 +1,51 @@ +.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. +.PP +It is important to note that under \fBLinux\fR based kernels, tap devices do +not receive packets unless the specific tuntap device has been opened by an +application. This requires \fBCAP_NET_ADMIN\fR privileges, so the +\fBovs-tcpdump\fR command must be run as a user with such permissions (this +is usually a super-user). +. +.SH "OPTIONS" +.so lib/common.man +. +.IP "\fB\-\-db\-sock\fR" +The Open vSwitch database socket connection string. The default is +\fIunix:@RUNDIR@/db.sock\fR +. +.IP "\fB\-\-dump\-cmd\fR" +The command to run instead of \fBtcpdump\fR. +. +.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\-\-mirror\-to\fR" +The name of the interface which should be the destination of the mirrored +packets. The default is miINTERFACE +. +.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..ba6ee9f --- /dev/null +++ b/utilities/ovs-tcpdump.in @@ -0,0 +1,453 @@ +#! /usr/bin/env @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 fcntl +import netifaces +import os +import pwd +import select +import struct +import subprocess +import sys +import time + +try: + from ovs.db import idl + from ovs import jsonrpc + from ovs.poller import Poller + from ovs.stream import Stream +except Exception: + print("ERROR: Please install the correct Open vSwitch python support") + print(" libraries (version @VERSION@).") + print(" Alternatively, check that your PYTHONPATH is pointing to") + print(" the correct location.") + sys.exit(1) + +tapdev_fd = None +_make_taps = {} + + +def _doexec(*args, **kwargs): + """Executes an application and returns a set of pipes""" + + shell = len(args) == 1 + proc = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell, + bufsize=0) + return proc + + +def _install_tap_linux(tap_name): + """Uses /dev/net/tun to create a tap device""" + global tapdev_fd + + IFF_TAP = 0x0002 + IFF_NO_PI = 0x1000 + TUNSETIFF = 0x400454CA # This is derived by printf() of TUNSETIFF + TUNSETOWNER = TUNSETIFF + 2 + + tapdev_fd = open('/dev/net/tun', 'rw') + ifr = struct.pack('16sH', tap_name, IFF_TAP | IFF_NO_PI) + fcntl.ioctl(tapdev_fd, TUNSETIFF, ifr) + fcntl.ioctl(tapdev_fd, TUNSETOWNER, os.getegid()) + + time.sleep(1) # required to give the new device settling time + pipe = _doexec( + *(['ip', 'link', 'set', 'dev', str(tap_name), 'up'])) + pipe.wait() + +_make_taps['linux'] = _install_tap_linux +_make_taps['linux2'] = _install_tap_linux + + +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 + --db-sock A connection string to reach the Open vSwitch + ovsdb-server. + Default 'unix:@RUNDIR@/db.sock' + --dump-cmd Command to use for tcpdump (default 'tcpdump') + -i, --interface Open vSwitch interface to mirror and tcpdump + --mirror-to The name for the mirror port to use (optional) + Default 'miINTERFACE' +""" % {'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 Exception: + 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: %s' % + (intf_name, txn.get_error())) + 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: %s' % + (port_name, txn.get_error())) + 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: + raise OVSDBException('Unable to delete Mirror %s: %s' % + (mirror_name, txn.get_error())) + 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: + 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@/db.sock' + interface = None + tcpdargs = [] + + skip_next = False + mirror_interface = None + dump_cmd = 'tcpdump' + + 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 ['--db-sock']: + db_sock = nxt + skip_next = True + continue + elif cur in ['--dump-cmd']: + dump_cmd = nxt + skip_next = True + continue + elif cur in ['-i', '--interface']: + interface = nxt + skip_next = True + continue + elif cur in ['--mirror-to']: + mirror_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') + + if '-vv' in tcpdargs: + print("TCPDUMP Args: %s" % ' '.join(tcpdargs)) + + ovsdb = OVSDB(db_sock) + mirror_interface = mirror_interface or "mi%s" % interface + + if sys.platform in _make_taps and \ + mirror_interface not in netifaces.interfaces(): + _make_taps[sys.platform](mirror_interface) + + if mirror_interface not in netifaces.interfaces(): + print("ERROR: Please create an interface called `%s`" % + mirror_interface) + print("See your OS guide for how to do this.") + print("Ex: ip link add %s type veth peer name %s" % + (mirror_interface, mirror_interface + "2")) + 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)) + try: + ovsdb.destroy_port(mirror_interface, ovsdb.port_bridge(interface)) + except Exception: + pass + sys.exit(1) + + pipes = _doexec(*([dump_cmd, '-i', mirror_interface] + tcpdargs)) + try: + while True: + print(pipes.stdout.readline()) + if select.select([sys.stdin], [], [], 0.0)[0]: + data_in = sys.stdin.read() + pipes.stdin.write(data_in) + except KeyboardInterrupt: + pipes.terminate() + ovsdb.destroy_mirror('m%s' % interface, ovsdb.port_bridge(interface)) + ovsdb.destroy_port(mirror_interface, ovsdb.port_bridge(interface)) + except Exception: + print("Unable to tear down the ports and mirrors.") + print("Please use ovs-vsctl to remove the ports and mirrors created.") + print(" ex: ovs-vsctl --db=%s del-port %s" % (db_sock, + mirror_interface)) + 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