X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=debian%2Fovs-monitor-ipsec;h=c12318801ca015bdf3f93d7b34c349651cc1bbaf;hb=8cdf0349740c3e1a73af9aa6209bb22be952cd37;hp=1cea8009b2ec45cde2a4ad96c4dc03c116b570c3;hpb=e97a10342018f992634fa90d25c007eb60c25662;p=sliver-openvswitch.git diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec index 1cea8009b..c12318801 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 Nicira Networks # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,32 +20,38 @@ # 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 glob import logging, logging.handlers import os -import stat +import socket 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 - -# 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) - +try: + # By default log messages as DAEMON into syslog + 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) +except socket.error, e: + logging.basicConfig() + s_log.warn("failed to connect to syslog (%s)" % e) setkey = "/usr/sbin/setkey" @@ -53,75 +59,203 @@ setkey = "/usr/sbin/setkey" 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"; + +""" + + # 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; +} + +""" -remote anonymous { + # 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(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/ovs-*.pem" % self.cert_dir): + try: + os.remove(ovs_cert) + except OSError: + s_log.warning("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"]) 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. + s_log.warning("attempting to restart racoon") + exitcode = subprocess.call(["/etc/init.d/racoon", "restart"]) + if exitcode != 0: + s_log.warning("couldn't reload racoon") + + def commit(self): + # Rewrite the Racoon configuration file + conf_file = open(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(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(vals["certificate"]): + raise error.Error("'certificate' file does not exist: %s" + % vals["certificate"]) + elif not os.path.isfile(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(vals["certificate"]).read() + if cert.find("-----BEGIN CERTIFICATE-----") == -1: + raise error.Error("'certificate' is not in valid PEM format") + + cert = open(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. + peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host) + f = open(peer_cert_file, "w") + f.write(vals["peer_cert"]) + f.close() + + vals["peer_cert_file"] = peer_cert_file + + 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(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,6 +266,7 @@ class IPsec: self.sad_flush() self.spd_flush() self.racoon = Racoon() + self.entries = [] def call_setkey(self, cmds): try: @@ -190,38 +325,35 @@ class IPsec: self.call_setkey("spdflush;") 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;" % (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 out;\n" % (local_ip, remote_ip) 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 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): @@ -245,8 +377,10 @@ def keep_table_columns(schema, table_name, column_types): table.columns = new_columns return table -def monitor_uuid_schema_cb(schema): +def prune_schema(schema): string_type = types.Type(types.BaseType(types.StringType)) + optional_ssl_type = types.Type(types.BaseType(types.UuidType, + ref_table_name='SSL'), None, 0, 1) string_map_type = types.Type(types.BaseType(types.StringType), types.BaseType(types.StringType), 0, sys.maxint) @@ -255,8 +389,12 @@ def monitor_uuid_schema_cb(schema): new_tables["Interface"] = keep_table_columns( schema, "Interface", {"name": string_type, "type": string_type, - "options": string_map_type, - "other_config": string_map_type}) + "options": string_map_type}) + new_tables["Open_vSwitch"] = keep_table_columns( + schema, "Open_vSwitch", {"ssl": optional_ssl_type}) + new_tables["SSL"] = keep_table_columns( + schema, "SSL", {"certificate": string_type, + "private_key": string_type}) schema.tables = new_tables def usage(): @@ -267,10 +405,38 @@ def usage(): print " -h, --help display this help message" sys.exit(0) +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. + if vals == orig_vals: + continue + else: + ipsec.del_entry(vals["local_ip"], vals["remote_ip"]) + + try: + ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals) + except error.Error, msg: + s_log.warning("skipping ipsec config for %s: %s" % (name, msg)) + +def get_ssl_cert(data): + for ovs_rec in data["Open_vSwitch"].rows.itervalues(): + ssl = ovs_rec.ssl + if ssl and ssl.certificate and ssl.private_key: + return (ssl.certificate, ssl.private_key) + + return None + def main(argv): try: options, args = getopt.gnu_getopt( - argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS) + argv[1:], 'h', ['help', 'root-prefix='] + ovs.daemon.LONG_OPTIONS) except getopt.GetoptError, geo: sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg)) sys.exit(1) @@ -278,6 +444,9 @@ def main(argv): for key, value in options: if key in ['-h', '--help']: usage() + elif key == '--root-prefix': + global root_prefix + root_prefix = value elif not ovs.daemon.parse_opt(key, value): sys.stderr.write("%s: unhandled option %s\n" % (ovs.util.PROGRAM_NAME, key)) @@ -288,10 +457,12 @@ def main(argv): "(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) + + schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR + schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file)) + prune_schema(schema) + idl = ovs.db.idl.Idl(remote, schema) ovs.daemon.daemonize() @@ -304,43 +475,46 @@ def main(argv): idl.wait(poller) poller.block() continue + + 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": + name = rec.name + options = rec.options + 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"), + "psk": options.get("psk") } + + if entry["peer_cert"] and entry["psk"]: + s_log.warning("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"]: + s_log.warning("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: + s_log.warning("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 if __name__ == '__main__':