#!/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; nat_traversal on; 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}) 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() 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) 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"]) 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)