From a3acf0b0c46a28d6c891086e054d81dd915eea2e Mon Sep 17 00:00:00 2001 From: Justin Pettit Date: Thu, 16 Sep 2010 19:19:11 -0700 Subject: [PATCH] debian: Add support for GRE-over-IPsec The ovs-monitor-ipsec daemon monitors the Interface table for GRE entries. If an entry specifies other-config parameters "ipsec-local-ip" and ("ipsec-psk" or "ipsec-cert"), it will create the appropriate security associations so that all GRE traffic to the remote host will be encrypted. In order for the two GRE tunnels to communicate, both sides need to be configured for IPsec with appropriate authentication. Currently, ovs-monitor-ipsec does not support certificate authentication or ensure that an interface is actually attached to a bridge. Both of these issues will be addressed in a forthcoming patch. NB: While GRE-over-IPsec should work on any system with a relatively recent racoon and setkey, it has only been tested on Debian. As such, only Debian packaging has been provided. --- debian/.gitignore | 1 + debian/automake.mk | 4 + debian/control | 16 +- debian/openvswitch-ipsec.dirs | 1 + debian/openvswitch-ipsec.init | 184 ++++++++++++++++ debian/openvswitch-ipsec.install | 1 + debian/ovs-monitor-ipsec | 349 +++++++++++++++++++++++++++++++ vswitchd/vswitch.ovsschema | 2 + vswitchd/vswitch.xml | 21 ++ 9 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 debian/openvswitch-ipsec.dirs create mode 100755 debian/openvswitch-ipsec.init create mode 100644 debian/openvswitch-ipsec.install create mode 100755 debian/ovs-monitor-ipsec diff --git a/debian/.gitignore b/debian/.gitignore index 7f43aa6e..24e62d94 100644 --- a/debian/.gitignore +++ b/debian/.gitignore @@ -12,6 +12,7 @@ /openvswitch-controller /openvswitch-datapath-source /openvswitch-dbg +/openvswitch-ipsec /openvswitch-pki /openvswitch-pki-server /openvswitch-switch diff --git a/debian/automake.mk b/debian/automake.mk index c768d56b..20432062 100644 --- a/debian/automake.mk +++ b/debian/automake.mk @@ -24,6 +24,9 @@ EXTRA_DIST += \ debian/openvswitch-datapath-source.copyright \ debian/openvswitch-datapath-source.dirs \ debian/openvswitch-datapath-source.install \ + debian/openvswitch-ipsec.dirs \ + debian/openvswitch-ipsec.init \ + debian/openvswitch-ipsec.install \ debian/openvswitch-pki-server.apache2 \ debian/openvswitch-pki-server.dirs \ debian/openvswitch-pki-server.install \ @@ -39,6 +42,7 @@ EXTRA_DIST += \ debian/openvswitch-switch.postrm \ debian/openvswitch-switch.template \ debian/ovs-bugtool \ + debian/ovs-monitor-ipsec \ debian/python-openvswitch.dirs \ debian/python-openvswitch.install \ debian/rules \ diff --git a/debian/control b/debian/control index 53e5b98d..622daeb3 100644 --- a/debian/control +++ b/debian/control @@ -41,6 +41,19 @@ Description: Open vSwitch switch implementations . Open vSwitch is a full-featured software-based Ethernet switch. +Package: openvswitch-ipsec +Architecture: any +Depends: + ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, ipsec-tools, racoon, + openvswitch-common (= ${binary:Version}), + openvswitch-switch (= ${binary:Version}), + python-openvswitch (= ${binary:Version}) +Description: Open vSwitch GRE-over-IPsec support + The ovs-monitor-ipsec script provides support for encrypting GRE + tunnels with IPsec. + . + Open vSwitch is a full-featured software-based Ethernet switch. + Package: openvswitch-pki Architecture: all Depends: @@ -90,13 +103,14 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, openvswitch-common (= ${binary:Version}), openvswitch-controller (= ${binary:Version}), + openvswitch-ipsec (= ${binary:Version}), openvswitch-switch (= ${binary:Version}) Description: Debug symbols for Open vSwitch packages This package contains the debug symbols for all the other openvswitch-* packages. Install it to debug one of them or to examine a core dump produced by one of them. -Package: python-openvswitch +Package: python-openvswitch Architecture: all Section: python Depends: ${python:Depends}, openvswitch-switch (= ${binary:Version}) diff --git a/debian/openvswitch-ipsec.dirs b/debian/openvswitch-ipsec.dirs new file mode 100644 index 00000000..02130d0e --- /dev/null +++ b/debian/openvswitch-ipsec.dirs @@ -0,0 +1 @@ +usr/share/openvswitch/scripts diff --git a/debian/openvswitch-ipsec.init b/debian/openvswitch-ipsec.init new file mode 100755 index 00000000..f3c9a13a --- /dev/null +++ b/debian/openvswitch-ipsec.init @@ -0,0 +1,184 @@ +#!/bin/sh +# +# Copyright (c) 2007, 2009 Javier Fernandez-Sanguino +# +# This is free software; you may redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2, +# or (at your option) any later version. +# +# This is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License with +# the Debian operating system, in /usr/share/common-licenses/GPL; if +# not, write to the Free Software Foundation, Inc., 59 Temple Place, +# Suite 330, Boston, MA 02111-1307 USA +# +### BEGIN INIT INFO +# Provides: openvswitch-ipsec +# Required-Start: $network $local_fs $remote_fs +# Required-Stop: $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Open vSwitch GRE-over-IPsec daemon +### END INIT INFO + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +DAEMON=/usr/share/openvswitch/scripts/ovs-monitor-ipsec # Daemon's location +NAME=ovs-monitor-ipsec # Introduce the short server's name here +LOGDIR=/var/log/openvswitch # Log directory to use + +PIDFILE=/var/run/openvswitch/$NAME.pid + +test -x $DAEMON || exit 0 + +. /lib/lsb/init-functions + +DODTIME=10 # Time to wait for the server to die, in seconds + # If this value is set too low you might not + # let some servers to die gracefully and + # 'restart' will not work + +set -e + +running_pid() { +# Check if a given process pid's cmdline matches a given name + pid=$1 + name=$2 + [ -z "$pid" ] && return 1 + [ ! -d /proc/$pid ] && return 1 + cmd=`cat /proc/$pid/cmdline | tr "\000" " "|cut -d " " -f 2` + # Is this the expected server + [ "$cmd" != "$name" ] && return 1 + return 0 +} + +running() { +# Check if the process is running looking at /proc +# (works for all users) + + # No pidfile, probably no daemon present + [ ! -f "$PIDFILE" ] && return 1 + pid=`cat $PIDFILE` + running_pid $pid $DAEMON || return 1 + return 0 +} + +start_server() { + PYTHONPATH=/usr/share/openvswitch/python \ + /usr/share/openvswitch/scripts/ovs-monitor-ipsec \ + --pidfile-name=$PIDFILE --detach --monitor \ + unix:/var/run/openvswitch/db.sock + + return 0 +} + +stop_server() { + if [ -e $PIDFILE ]; then + kill `cat $PIDFILE` + fi + + return 0 +} + +force_stop() { +# Force the process to die killing it manually + [ ! -e "$PIDFILE" ] && return + if running ; then + kill -15 $pid + # Is it really dead? + sleep "$DIETIME"s + if running ; then + kill -9 $pid + sleep "$DIETIME"s + if running ; then + echo "Cannot kill $NAME (pid=$pid)!" + exit 1 + fi + fi + fi + rm -f $PIDFILE +} + + +case "$1" in + start) + log_daemon_msg "Starting $NAME" + # Check if it's running first + if running ; then + log_progress_msg "apparently already running" + log_end_msg 0 + exit 0 + fi + if start_server && running ; then + # It's ok, the server started and is running + log_end_msg 0 + else + # Either we could not start it or it is not running + # after we did + # NOTE: Some servers might die some time after they start, + # this code does not try to detect this and might give + # a false positive (use 'status' for that) + log_end_msg 1 + fi + ;; + stop) + log_daemon_msg "Stopping $NAME" + if running ; then + # Only stop the server if we see it running + stop_server + log_end_msg $? + else + # If it's not running don't do anything + log_progress_msg "apparently not running" + log_end_msg 0 + exit 0 + fi + ;; + force-stop) + # First try to stop gracefully the program + $0 stop + if running; then + # If it's still running try to kill it more forcefully + log_daemon_msg "Stopping (force) $NAME" + force_stop + log_end_msg $? + fi + ;; + restart|force-reload) + log_daemon_msg "Restarting $NAME" + stop_server + # Wait some sensible amount, some server need this + [ -n "$DIETIME" ] && sleep $DIETIME + start_server + running + log_end_msg $? + ;; + status) + log_daemon_msg "Checking status of $NAME" + if running ; then + log_progress_msg "running" + log_end_msg 0 + else + log_progress_msg "apparently not running" + log_end_msg 1 + exit 1 + fi + ;; + # Use this if the daemon cannot reload + reload) + log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon" + log_warning_msg "cannot re-read the config file (use restart)." + ;; + *) + N=/etc/init.d/openvswitch-ipsec + echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/debian/openvswitch-ipsec.install b/debian/openvswitch-ipsec.install new file mode 100644 index 00000000..72cacfa2 --- /dev/null +++ b/debian/openvswitch-ipsec.install @@ -0,0 +1 @@ +debian/ovs-monitor-ipsec usr/share/openvswitch/scripts diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec new file mode 100755 index 00000000..1caece3a --- /dev/null +++ b/debian/ovs-monitor-ipsec @@ -0,0 +1,349 @@ +#!/usr/bin/python +# Copyright (c) 2009, 2010 Nicira Networks +# +# 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. + + +# A daemon to monitor attempts to create GRE-over-IPsec tunnels. +# Uses racoon and setkey to support the configuration. Assumes that +# OVS has complete control over IPsec configuration for the box. + +# xxx To-do: +# - Doesn't actually check that Interface is connected to bridge +# - Doesn't support cert authentication + + +import getopt +import logging, logging.handlers +import os +import stat +import subprocess +import sys + +from ovs.db import error +from ovs.db import types +import ovs.util +import ovs.daemon +import ovs.db.idl + + +# By default log messages as DAEMON into syslog +s_log = logging.getLogger("ovs-monitor-ipsec") +l_handler = logging.handlers.SysLogHandler( + "/dev/log", + facility=logging.handlers.SysLogHandler.LOG_DAEMON) +l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s') +l_handler.setFormatter(l_formatter) +s_log.addHandler(l_handler) + + +setkey = "/usr/sbin/setkey" + +# Class to configure the racoon daemon, which handles IKE negotiation +class Racoon: + # Default locations for files + conf_file = "/etc/racoon/racoon.conf" + cert_file = "/etc/racoon/certs" + psk_file = "/etc/racoon/psk.txt" + + # Default racoon configuration file we use for IKE + conf_template = """# Configuration file generated by Open vSwitch +# +# Do not modify by hand! + +path pre_shared_key "/etc/racoon/psk.txt"; +path certificate "/etc/racoon/certs"; + +remote anonymous { + exchange_mode main; + proposal { + encryption_algorithm aes; + hash_algorithm sha1; + authentication_method pre_shared_key; + dh_group 2; + } +} + +sainfo anonymous { + pfs_group 2; + lifetime time 1 hour; + encryption_algorithm aes; + authentication_algorithm hmac_sha1, hmac_md5; + compression_algorithm deflate; +} +""" + + def __init__(self): + self.psk_hosts = {} + self.cert_hosts = {} + + # Replace racoon's conf file with our template + f = open(Racoon.conf_file, "w") + f.write(Racoon.conf_template) + f.close() + + # Clear out any pre-shared keys + self.commit_psk() + + self.reload() + + def reload(self): + exitcode = subprocess.call(["/etc/init.d/racoon", "reload"]) + if exitcode != 0: + s_log.warning("couldn't reload racoon") + + def commit_psk(self): + f = open(Racoon.psk_file, 'w') + + # The file must only be accessible by root + os.chmod(Racoon.psk_file, stat.S_IRUSR | stat.S_IWUSR) + + f.write("# Generated by Open vSwitch...do not modify by hand!\n\n") + for host, psk in self.psk_hosts.iteritems(): + f.write("%s %s\n" % (host, psk)) + f.close() + + def add_psk(self, host, psk): + self.psk_hosts[host] = psk + self.commit_psk() + + def del_psk(self, host): + if host in self.psk_hosts: + del self.psk_hosts[host] + self.commit_psk() + + +# Class to configure IPsec on a system using racoon for IKE and setkey +# for maintaining the Security Association Database (SAD) and Security +# Policy Database (SPD). Only policies for GRE are supported. +class IPsec: + def __init__(self): + self.sad_flush() + self.spd_flush() + self.racoon = Racoon() + + def call_setkey(self, cmds): + try: + p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + except: + s_log.error("could not call setkey") + sys.exit(1) + + # xxx It is safer to pass the string into the communicate() + # xxx method, but it didn't work for slightly longer commands. + # xxx An alternative may need to be found. + p.stdin.write(cmds) + return p.communicate()[0] + + def get_spi(self, local_ip, remote_ip, proto="esp"): + # Run the setkey dump command to retrieve the SAD. Then, parse + # the output looking for SPI buried in the output. Note that + # multiple SAD entries can exist for the same "flow", since an + # older entry could be in a "dying" state. + spi_list = [] + host_line = "%s %s" % (local_ip, remote_ip) + results = self.call_setkey("dump ;").split("\n") + for i in range(len(results)): + if results[i].strip() == host_line: + # The SPI is in the line following the host pair + spi_line = results[i+1] + if (spi_line[1:4] == proto): + spi = spi_line.split()[2] + spi_list.append(spi.split('(')[1].rstrip(')')) + return spi_list + + def sad_flush(self): + self.call_setkey("flush;") + + def sad_del(self, local_ip, remote_ip): + # To delete all SAD entries, we should be able to use setkey's + # "deleteall" command. Unfortunately, it's fundamentally broken + # on Linux and not documented as such. + cmds = "" + + # Delete local_ip->remote_ip SAD entries + spi_list = self.get_spi(local_ip, remote_ip) + for spi in spi_list: + cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi) + + # Delete remote_ip->local_ip SAD entries + spi_list = self.get_spi(remote_ip, local_ip) + for spi in spi_list: + cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi) + + if cmds: + self.call_setkey(cmds) + + def spd_flush(self): + self.call_setkey("spdflush;") + + def spd_add(self, local_ip, remote_ip): + cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;" % + (local_ip, remote_ip)) + cmds += "\n" + cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" % + (remote_ip, local_ip)) + self.call_setkey(cmds) + + def spd_del(self, local_ip, remote_ip): + cmds = "spddelete %s %s gre -P out;" % (local_ip, remote_ip) + cmds += "\n" + cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip) + self.call_setkey(cmds) + + def ipsec_cert_del(self, local_ip, remote_ip): + # Need to support cert...right now only PSK supported + self.racoon.del_psk(remote_ip) + self.spd_del(local_ip, remote_ip) + self.sad_del(local_ip, remote_ip) + + def ipsec_cert_update(self, local_ip, remote_ip, cert): + # Need to support cert...right now only PSK supported + self.racoon.add_psk(remote_ip, "abc12345") + self.spd_add(local_ip, remote_ip) + + def ipsec_psk_del(self, local_ip, remote_ip): + self.racoon.del_psk(remote_ip) + self.spd_del(local_ip, remote_ip) + self.sad_del(local_ip, remote_ip) + + def ipsec_psk_update(self, local_ip, remote_ip, psk): + self.racoon.add_psk(remote_ip, psk) + self.spd_add(local_ip, remote_ip) + + +def keep_table_columns(schema, table_name, column_types): + table = schema.tables.get(table_name) + if not table: + raise error.Error("schema has no %s table" % table_name) + + new_columns = {} + for column_name, column_type in column_types.iteritems(): + column = table.columns.get(column_name) + if not column: + raise error.Error("%s table schema lacks %s column" + % (table_name, column_name)) + if column.type != column_type: + raise error.Error("%s column in %s table has type \"%s\", " + "expected type \"%s\"" + % (column_name, table_name, + column.type.toEnglish(), + column_type.toEnglish())) + new_columns[column_name] = column + table.columns = new_columns + return table + +def monitor_uuid_schema_cb(schema): + string_type = types.Type(types.BaseType(types.StringType)) + string_map_type = types.Type(types.BaseType(types.StringType), + types.BaseType(types.StringType), + 0, sys.maxint) + + new_tables = {} + new_tables["Interface"] = keep_table_columns( + schema, "Interface", {"name": string_type, + "type": string_type, + "options": string_map_type, + "other_config": string_map_type}) + schema.tables = new_tables + +def usage(): + print "usage: %s [OPTIONS] DATABASE" % sys.argv[0] + print "where DATABASE is a socket on which ovsdb-server is listening." + ovs.daemon.usage() + print "Other options:" + print " -h, --help display this help message" + sys.exit(0) + +def main(argv): + try: + options, args = getopt.gnu_getopt( + argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS) + except getopt.GetoptError, geo: + sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg)) + sys.exit(1) + + for key, value in options: + if key in ['-h', '--help']: + usage() + elif not ovs.daemon.parse_opt(key, value): + sys.stderr.write("%s: unhandled option %s\n" + % (ovs.util.PROGRAM_NAME, key)) + sys.exit(1) + + if len(args) != 1: + sys.stderr.write("%s: exactly one nonoption argument is required " + "(use --help for help)\n" % ovs.util.PROGRAM_NAME) + sys.exit(1) + + ovs.daemon.die_if_already_running() + + remote = args[0] + idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb) + + ovs.daemon.daemonize() + + ipsec = IPsec() + + interfaces = {} + while True: + if not idl.run(): + poller = ovs.poller.Poller() + idl.wait(poller) + poller.block() + continue + + new_interfaces = {} + for rec in idl.data["Interface"].itervalues(): + name = rec.name.as_scalar() + local_ip = rec.other_config.get("ipsec_local_ip") + if rec.type.as_scalar() == "gre" and local_ip: + new_interfaces[name] = { + "remote_ip": rec.options.get("remote_ip"), + "local_ip": local_ip, + "ipsec_cert": rec.other_config.get("ipsec_cert"), + "ipsec_psk": rec.other_config.get("ipsec_psk") } + + if interfaces != new_interfaces: + for name, vals in interfaces.items(): + if name not in new_interfaces.keys(): + ipsec.ipsec_cert_del(vals["local_ip"], vals["remote_ip"]) + for name, vals in new_interfaces.items(): + if vals == interfaces.get(name): + s_log.warning( + "configuration changed for %s, need to delete " + "interface first" % name) + continue + + if vals["ipsec_cert"]: + ipsec.ipsec_cert_update(vals["local_ip"], + vals["remote_ip"], vals["ipsec_cert"]) + elif vals["ipsec_psk"]: + ipsec.ipsec_psk_update(vals["local_ip"], + vals["remote_ip"], vals["ipsec_psk"]) + else: + s_log.warning( + "no ipsec_cert or ipsec_psk defined for %s" % name) + continue + + interfaces = new_interfaces + +if __name__ == '__main__': + try: + main(sys.argv) + except SystemExit: + # Let system.exit() calls complete normally + raise + except: + s_log.exception("traceback") diff --git a/vswitchd/vswitch.ovsschema b/vswitchd/vswitch.ovsschema index a7d25703..07dd79fb 100644 --- a/vswitchd/vswitch.ovsschema +++ b/vswitchd/vswitch.ovsschema @@ -134,6 +134,8 @@ "ofport": { "type": {"key": "integer", "min": 0, "max": 1}, "ephemeral": true}, + "other_config": { + "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "statistics": { "type": {"key": "string", "value": "integer", "min": 0, "max": "unlimited"}, "ephemeral": true}, diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml index 86fd3f9b..5b5655dd 100644 --- a/vswitchd/vswitch.xml +++ b/vswitchd/vswitch.xml @@ -775,6 +775,27 @@ + + Key-value pairs for rarely used interface features. Currently, + the only keys are for configuring GRE-over-IPsec, which is only + available through the openvswitch-ipsec package for + Debian. The currently defined key-value pairs are: +
+
ipsec-local-ip
+
Required key for GRE-over-IPsec interfaces. Additionally, + the must be gre and the + ipsec-psk key must + be set. The in_key, out_key, and + key must not be + set.
+
ipsec-psk
+
Required key for GRE-over-IPsec interfaces. Specifies a + pre-shared key for authentication that must be identical on + both sides of the tunnel. Additionally, the + ipsec-local-ip key must also be set.
+
+
+

Key-value pairs that report interface statistics. The current -- 2.30.2