#!/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()
+
+ self.reload()
- 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)
+ def _add_psk(self, host, psk):
+ if host in self.cert_hosts:
+ raise error.Error("host %s already defined for cert" % host)
- 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))
+ self.psk_hosts[host] = psk
+ 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()
- def add_psk(self, host, psk):
- self.psk_hosts[host] = psk
- self.commit_psk()
+ self.cert_hosts[host] = vals
+ self.commit()
- def del_psk(self, host):
- if host in self.psk_hosts:
+ 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
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()
# 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
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 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 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})
- 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 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.options.get("ipsec_cert")
- ipsec_psk = rec.options.get("ipsec_psk")
- is_ipsec = ipsec_cert or ipsec_psk
-
- if rec.type.as_scalar() == "ipsec_gre":
- if ipsec_cert or ipsec_psk:
- 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 }
- else:
- s_log.warning(
- "no ipsec_cert or ipsec_psk defined for %s" % name)
-
- 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():
- orig_vals = interfaces.get(name):
- if orig_vals:
- # Configuration for this host already exists. If
- # it has changed, this is an error.
- if vals != orig_vals:
- 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
+ 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
+ elif not entry["peer_cert"] and not entry["psk"]:
+ vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
continue
- if vals["ipsec_cert"]:
- ipsec.ipsec_cert_update(vals["local_ip"],
- vals["remote_ip"], vals["ipsec_cert"])
- else:
- ipsec.ipsec_psk_update(vals["local_ip"],
- vals["remote_ip"], vals["ipsec_psk"])
+ # 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)