ofproto: Fix potential leak during flow mods.
[sliver-openvswitch.git] / debian / ovs-monitor-ipsec
index 1cea800..414d18b 100755 (executable)
@@ -1,5 +1,5 @@
 #!/usr/bin/python
-# Copyright (c) 2009, 2010 Nicira Networks
+# Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 
 # xxx To-do:
 #  - Doesn't actually check that Interface is connected to bridge
-#  - Doesn't support cert authentication
+#  - If a certificate is badly formed, Racoon will refuse to start.  We
+#    should do a better job of verifying certificates are valid before
+#    adding an interface to racoon.conf.
 
 
-import getopt
-import logging, logging.handlers
+import argparse
+import glob
 import os
-import stat
 import subprocess
 import sys
 
+import ovs.dirs
 from ovs.db import error
 from ovs.db import types
 import ovs.util
 import ovs.daemon
 import ovs.db.idl
+import ovs.unixctl
+import ovs.unixctl.server
+import ovs.vlog
 
+vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
+root_prefix = ''                # Prefix for absolute file names, for testing.
+SETKEY = "/usr/sbin/setkey"
+exiting = False
 
-# 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)
 
+def unixctl_exit(conn, unused_argv, unused_aux):
+    global exiting
+    exiting = True
+    conn.reply(None)
 
-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"
+    cert_dir = "/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
+    # Racoon configuration header we use for IKE
+    conf_header = """# Configuration file generated by Open vSwitch
 #
 # Do not modify by hand!
 
-path pre_shared_key "/etc/racoon/psk.txt";
-path certificate "/etc/racoon/certs";
+path pre_shared_key "%s";
+path certificate "%s";
 
-remote anonymous {
+"""
+
+    # Racoon configuration footer we use for IKE
+    conf_footer = """sainfo anonymous {
+        pfs_group 2;
+        lifetime time 1 hour;
+        encryption_algorithm aes;
+        authentication_algorithm hmac_sha1, hmac_md5;
+        compression_algorithm deflate;
+}
+
+"""
+
+    # Certificate entry template.
+    cert_entry = """remote %s {
         exchange_mode main;
         nat_traversal on;
+        ike_frag on;
+        certificate_type x509 "%s" "%s";
+        my_identifier asn1dn;
+        peers_identifier asn1dn;
+        peers_certfile x509 "%s";
+        verify_identifier on;
         proposal {
                 encryption_algorithm aes;
                 hash_algorithm sha1;
-                authentication_method pre_shared_key;
+                authentication_method rsasig;
                 dh_group 2;
         }
 }
 
-sainfo anonymous {
-        pfs_group 2;
-        lifetime time 1 hour;
-        encryption_algorithm aes;
-        authentication_algorithm hmac_sha1, hmac_md5;
-        compression_algorithm deflate;
+"""
+
+    # Pre-shared key template.
+    psk_entry = """remote %s {
+        exchange_mode main;
+        nat_traversal on;
+        proposal {
+                encryption_algorithm aes;
+                hash_algorithm sha1;
+                authentication_method pre_shared_key;
+                dh_group 2;
+        }
 }
+
 """
 
     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()
+        if not os.path.isdir(root_prefix + self.cert_dir):
+            os.mkdir(self.cert_dir)
 
-        # Clear out any pre-shared keys
-        self.commit_psk()
+        # Clean out stale peer certs from previous runs
+        for ovs_cert in glob.glob("%s%s/ovs-*.pem"
+                                  % (root_prefix, self.cert_dir)):
+            try:
+                os.remove(ovs_cert)
+            except OSError:
+                vlog.warn("couldn't remove %s" % ovs_cert)
 
-        self.reload()
+        # Replace racoon's conf file with our template
+        self.commit()
 
     def reload(self):
-        exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
+        exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
+                                    "reload"])
         if exitcode != 0:
-            s_log.warning("couldn't reload racoon")
+            # Racoon is finicky about its configuration file and will
+            # refuse to start if it sees something it doesn't like
+            # (e.g., a certificate file doesn't exist).  Try restarting
+            # the process before giving up.
+            vlog.warn("attempting to restart racoon")
+            exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
+                                        "restart"])
+            if exitcode != 0:
+                vlog.warn("couldn't reload racoon")
+
+    def commit(self):
+        # Rewrite the Racoon configuration file
+        conf_file = open(root_prefix + self.conf_file, 'w')
+        conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
+
+        for host, vals in self.cert_hosts.iteritems():
+            conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
+                    vals["private_key"], vals["peer_cert_file"]))
+
+        for host in self.psk_hosts:
+            conf_file.write(Racoon.psk_entry % host)
+
+        conf_file.write(Racoon.conf_footer)
+        conf_file.close()
+
+        # Rewrite the pre-shared keys file; it must only be readable by root.
+        orig_umask = os.umask(0077)
+        psk_file = open(root_prefix + Racoon.psk_file, 'w')
+        os.umask(orig_umask)
+
+        psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
+        psk_file.write("\n\n")
+        for host, vals in self.psk_hosts.iteritems():
+            psk_file.write("%s   %s\n" % (host, vals["psk"]))
+        psk_file.close()
 
-    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)
+        self.reload()
 
-        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):
+        if host in self.cert_hosts:
+            raise error.Error("host %s already defined for cert" % host)
 
-    def add_psk(self, host, psk):
         self.psk_hosts[host] = psk
-        self.commit_psk()
-
-    def del_psk(self, host):
+        self.commit()
+
+    def _verify_certs(self, vals):
+        # Racoon will refuse to start if the certificate files don't
+        # exist, so verify that they're there.
+        if not os.path.isfile(root_prefix + vals["certificate"]):
+            raise error.Error("'certificate' file does not exist: %s"
+                    % vals["certificate"])
+        elif not os.path.isfile(root_prefix + vals["private_key"]):
+            raise error.Error("'private_key' file does not exist: %s"
+                    % vals["private_key"])
+
+        # Racoon won't start if a given certificate or private key isn't
+        # valid.  This is a weak test, but will detect the most flagrant
+        # errors.
+        if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
+            raise error.Error("'peer_cert' is not in valid PEM format")
+
+        cert = open(root_prefix + vals["certificate"]).read()
+        if cert.find("-----BEGIN CERTIFICATE-----") == -1:
+            raise error.Error("'certificate' is not in valid PEM format")
+
+        cert = open(root_prefix + vals["private_key"]).read()
+        if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
+            raise error.Error("'private_key' is not in valid PEM format")
+
+    def _add_cert(self, host, vals):
         if host in self.psk_hosts:
+            raise error.Error("host %s already defined for psk" % host)
+
+        if vals["certificate"] == None:
+            raise error.Error("'certificate' not defined for %s" % host)
+        elif vals["private_key"] == None:
+            # Assume the private key is stored in the same PEM file as
+            # the certificate.  We make a copy of "vals" so that we don't
+            # modify the original "vals", which would cause the script
+            # to constantly think that the configuration has changed
+            # in the database.
+            vals = vals.copy()
+            vals["private_key"] = vals["certificate"]
+
+        self._verify_certs(vals)
+
+        # The peer's certificate comes to us in PEM format as a string.
+        # Write that string to a file for Racoon to use.
+        f = open(root_prefix + vals["peer_cert_file"], "w")
+        f.write(vals["peer_cert"])
+        f.close()
+
+        self.cert_hosts[host] = vals
+        self.commit()
+
+    def _del_cert(self, host):
+        peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
+        del self.cert_hosts[host]
+        self.commit()
+        try:
+            os.remove(root_prefix + peer_cert_file)
+        except OSError:
+            pass
+
+    def add_entry(self, host, vals):
+        if vals["peer_cert"]:
+            self._add_cert(host, vals)
+        elif vals["psk"]:
+            self._add_psk(host, vals)
+
+    def del_entry(self, host):
+        if host in self.cert_hosts:
+            self._del_cert(host)
+        elif host in self.psk_hosts:
             del self.psk_hosts[host]
-            self.commit_psk()
+            self.commit()
 
 
 # Class to configure IPsec on a system using racoon for IKE and setkey
@@ -132,13 +263,15 @@ class IPsec:
         self.sad_flush()
         self.spd_flush()
         self.racoon = Racoon()
+        self.entries = []
 
     def call_setkey(self, cmds):
         try:
-            p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, 
-                    stdout=subprocess.PIPE)
+            p = subprocess.Popen([root_prefix + SETKEY, "-c"],
+                                 stdin=subprocess.PIPE,
+                                 stdout=subprocess.PIPE)
         except:
-            s_log.error("could not call setkey")
+            vlog.err("could not call %s%s" % (root_prefix, SETKEY))
             sys.exit(1)
 
         # xxx It is safer to pass the string into the communicate()
@@ -154,18 +287,18 @@ class IPsec:
         # 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")
+        results = self.call_setkey("dump ;\n").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]
+                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;")
+        self.call_setkey("flush;\n")
 
     def sad_del(self, local_ip, remote_ip):
         # To delete all SAD entries, we should be able to use setkey's
@@ -187,168 +320,175 @@ class IPsec:
             self.call_setkey(cmds)
 
     def spd_flush(self):
-        self.call_setkey("spdflush;")
+        self.call_setkey("spdflush;\n")
 
     def spd_add(self, local_ip, remote_ip):
-        cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;" %
+        cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
                     (local_ip, remote_ip))
-        cmds += "\n"
-        cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" %
+        cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
                     (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)
+        cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
+        cmds += "spddelete %s %s gre -P in;\n" % (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 add_entry(self, local_ip, remote_ip, vals):
+        if remote_ip in self.entries:
+            raise error.Error("host %s already configured for ipsec"
+                              % 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.racoon.add_entry(remote_ip, vals)
         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)
+        self.entries.append(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 del_entry(self, local_ip, remote_ip):
+        if remote_ip in self.entries:
+            self.racoon.del_entry(remote_ip)
+            self.spd_del(local_ip, remote_ip)
+            self.sad_del(local_ip, remote_ip)
 
+            self.entries.remove(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)
+def update_ipsec(ipsec, interfaces, new_interfaces):
+    for name, vals in interfaces.iteritems():
+        if name not in new_interfaces:
+            ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
+
+    for name, vals in new_interfaces.iteritems():
+        orig_vals = interfaces.get(name)
+        if orig_vals:
+            # Configuration for this host already exists.  Check if it's
+            # changed.  We use set difference, since we want to ignore
+            # any local additions to "orig_vals" that we've made
+            # (e.g. the "peer_cert_file" key).
+            if set(vals.items()) - set(orig_vals.items()):
+                ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
+            else:
+                continue
+
+        try:
+            ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
+        except error.Error, msg:
+            vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
+
+
+def get_ssl_cert(data):
+    for ovs_rec in data["Open_vSwitch"].rows.itervalues():
+        if ovs_rec.ssl:
+            ssl = ovs_rec.ssl[0]
+            if ssl.certificate and ssl.private_key:
+                return (ssl.certificate, ssl.private_key)
+
+    return None
+
+
+def main():
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("database", metavar="DATABASE",
+                        help="A socket on which ovsdb-server is listening.")
+    parser.add_argument("--root-prefix", metavar="DIR",
+                        help="Use DIR as alternate root directory"
+                        " (for testing).")
+
+    ovs.vlog.add_args(parser)
+    ovs.daemon.add_args(parser)
+    args = parser.parse_args()
+    ovs.vlog.handle_args(args)
+    ovs.daemon.handle_args(args)
+
+    global root_prefix
+    if args.root_prefix:
+        root_prefix = args.root_prefix
+
+    remote = args.database
+    schema_helper = ovs.db.idl.SchemaHelper()
+    schema_helper.register_columns("Interface", ["name", "type", "options"])
+    schema_helper.register_columns("Open_vSwitch", ["ssl"])
+    schema_helper.register_columns("SSL", ["certificate", "private_key"])
+    idl = ovs.db.idl.Idl(remote, schema_helper)
 
     ovs.daemon.daemonize()
 
+    ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
+    error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
+    if error:
+        ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
+
     ipsec = IPsec()
 
     interfaces = {}
+    seqno = idl.change_seqno    # Sequence number when we last processed the db
     while True:
-        if not idl.run():
+        unixctl_server.run()
+        if exiting:
+            break
+
+        idl.run()
+        if seqno == idl.change_seqno:
             poller = ovs.poller.Poller()
+            unixctl_server.wait(poller)
             idl.wait(poller)
             poller.block()
             continue
+        seqno = idl.change_seqno
+
+        ssl_cert = get_ssl_cert(idl.tables)
+
         new_interfaces = {}
-        for rec in idl.data["Interface"].itervalues():
-            name = rec.name.as_scalar()
-            ipsec_cert = rec.other_config.get("ipsec_cert")
-            ipsec_psk = rec.other_config.get("ipsec_psk")
-            is_ipsec = ipsec_cert or ipsec_psk
-
-            if rec.type.as_scalar() == "gre" and is_ipsec:
-                new_interfaces[name] = {
-                        "remote_ip": rec.options.get("remote_ip"),
-                        "local_ip": rec.options.get("local_ip", "0.0.0.0/0"),
-                        "ipsec_cert": ipsec_cert,
-                        "ipsec_psk": 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)
+        for rec in idl.tables["Interface"].rows.itervalues():
+            if rec.type == "ipsec_gre" or rec.type == "ipsec_gre64":
+                name = rec.name
+                options = rec.options
+                peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
+                entry = {
+                    "remote_ip": options.get("remote_ip"),
+                    "local_ip": options.get("local_ip", "0.0.0.0/0"),
+                    "certificate": options.get("certificate"),
+                    "private_key": options.get("private_key"),
+                    "use_ssl_cert": options.get("use_ssl_cert"),
+                    "peer_cert": options.get("peer_cert"),
+                    "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
+                    "psk": options.get("psk")}
+
+                if entry["peer_cert"] and entry["psk"]:
+                    vlog.warn("both 'peer_cert' and 'psk' defined for %s"
+                              % 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)
+                elif not entry["peer_cert"] and not entry["psk"]:
+                    vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
                     continue
 
+                # The "use_ssl_cert" option is deprecated and will
+                # likely go away in the near future.
+                if entry["use_ssl_cert"] == "true":
+                    if not ssl_cert:
+                        vlog.warn("no valid SSL entry for %s" % name)
+                        continue
+
+                    entry["certificate"] = ssl_cert[0]
+                    entry["private_key"] = ssl_cert[1]
+
+                new_interfaces[name] = entry
+
+        if interfaces != new_interfaces:
+            update_ipsec(ipsec, interfaces, new_interfaces)
             interfaces = new_interfaces
+
+    unixctl_server.close()
+    idl.close()
+
+
 if __name__ == '__main__':
     try:
-        main(sys.argv)
+        main()
     except SystemExit:
         # Let system.exit() calls complete normally
         raise
     except:
-        s_log.exception("traceback")
+        vlog.exception("traceback")
         sys.exit(ovs.daemon.RESTART_EXIT_CODE)