X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=debian%2Fovs-monitor-ipsec;h=414d18bae8f5a06e55b3a4aa51f14b4216107613;hb=003ce655b7116d18c86a74c50391e54990346931;hp=1cea8009b2ec45cde2a4ad96c4dc03c116b570c3;hpb=e97a10342018f992634fa90d25c007eb60c25662;p=sliver-openvswitch.git diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec index 1cea8009b..414d18bae 100755 --- a/debian/ovs-monitor-ipsec +++ b/debian/ovs-monitor-ipsec @@ -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. @@ -20,108 +20,239 @@ # 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)