Merge "master" into "wdp".
[sliver-openvswitch.git] / debian / ovs-monitor-ipsec
diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec
new file mode 100755 (executable)
index 0000000..184b004
--- /dev/null
@@ -0,0 +1,350 @@
+#!/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")
+        sys.exit(ovs.daemon.RESTART_EXIT_CODE)