Search
SailfishOS Open Build Service
>
Projects
>
mer-infra:testing
>
openvswitch
> ovs-monitor-ipsec
Log In
Username
Password
Cancel
Overview
Repositories
Revisions
Requests
Users
Advanced
Attributes
Meta
File ovs-monitor-ipsec of Package openvswitch
#!/usr/bin/python # Copyright (c) 2009-2015 Nicira, Inc. # # 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. import argparse import glob import os import re import subprocess import sys from string import Template from ovs.db import error from ovs.db import types import ovs.daemon import ovs.db.idl import ovs.dirs import ovs.unixctl import ovs.unixctl.server import ovs.util import ovs.vlog FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n" vlog = ovs.vlog.Vlog("ovs-monitor-ipsec") exiting = False keyer = None xfrm = None def unixctl_xfrm_policies(conn, unused_argv, unused_aux): global xfrm policies = xfrm.get_policies() conn.reply(str(policies)) def unixctl_xfrm_state(conn, unused_argv, unused_aux): global xfrm securities = xfrm.get_securities() conn.reply(str(securities)) def unixctl_ipsec_status(conn, unused_argv, unused_aux): global keyer conns = keyer._get_strongswan_conns() conn.reply(str(conns)) def unixctl_show(conn, unused_argv, unused_aux): global keyer global xfrm policies = xfrm.get_policies() securities = xfrm.get_securities() keyer.show(conn, policies, securities) def unixctl_exit(conn, unused_argv, unused_aux): global exiting exiting = True conn.reply(None) class XFRM(object): """This class is a simple wrapper around ip-xfrm (8) command line utility. For now we are using this class only so that ovs-monitor-ipsec could verify that IKE keying daemon has installed IPsec policies and security associations into kernel.""" def __init__(self, ip_root_prefix): self.IP = ip_root_prefix + "/sbin/ip" def get_policies(self): """This function returns IPsec policies (from kernel) in a dictionary where <key> is destination IPv4 address and <value> is SELECTOR of the IPsec policy.""" policies = {} proc = subprocess.Popen([self.IP, 'xfrm', 'policy'], stdout=subprocess.PIPE) while True: line = proc.stdout.readline().strip() if line == '': break a = line.split(" ") if len(a) >= 4 and a[0] == "src" and a[2] == "dst": dst = (a[3].split("/"))[0] if not dst in policies: policies[dst] = [] policies[dst].append(line) src = (a[3].split("/"))[0] if not src in policies: policies[src] = [] policies[src].append(line) return policies def get_securities(self): """This function returns IPsec security associations (from kernel) in a dictionary where <key> is destination IPv4 address and <value> is SELECTOR.""" securities = {} proc = subprocess.Popen([self.IP, 'xfrm', 'state'], stdout=subprocess.PIPE) while True: line = proc.stdout.readline().strip() if line == '': break a = line.split(" ") if len(a) >= 4 and a[0] == "sel" and a[1] == "src" and a[3] == "dst": remote_ip = a[4].rstrip().split("/")[0] local_ip = a[2].rstrip().split("/")[0] if not remote_ip in securities: securities[remote_ip] = [] securities[remote_ip].append(line) if not local_ip in securities: securities[local_ip] = [] securities[local_ip].append(line) return securities class StrongSwanTunnel(object): """This class represents IPsec tunnel in strongSwan""" transp_tmpl = {"ipsec_gre" : Template("""\ conn $ifname-$version $auth_section leftsubnet=%dynamic[gre] rightsubnet=%dynamic[gre] """), "ipsec_gre64" : Template("""\ conn $ifname-$version $auth_section leftsubnet=%dynamic[gre] rightsubnet=%dynamic[gre] """), "ipsec_geneve" : Template("""\ conn $ifname-in-$version $auth_section rightsubnet=%dynamic[udp/%any] leftsubnet=%dynamic[udp/6081] conn $ifname-out-$version $auth_section rightsubnet=%dynamic[udp/6081] leftsubnet=%dynamic[udp/%any] """), "ipsec_stt" : Template("""\ conn $ifname-in-$version $auth_section rightsubnet=%dynamic[tcp/%any] leftsubnet=%dynamic[tcp/7471] conn $ifname-out-$version $auth_section rightsubnet=%dynamic[tcp/7471] leftsubnet=%dynamic[tcp/%any] """), "ipsec_vxlan" : Template("""\ conn $ifname-in-$version $auth_section rightsubnet=%dynamic[udp/%any] leftsubnet=%dynamic[udp/4789] conn $ifname-out-$version $auth_section rightsubnet=%dynamic[udp/4789] leftsubnet=%dynamic[udp/%any] """)} auth_tmpl = {"psk" : Template("""\ left=$local_ip right=$remote_ip authby=psk"""), "rsa" : Template("""\ left=$local_ip right=$remote_ip rightcert=ovs-$remote_ip.pem leftcert=$certificate""")} unixctl_config_tmpl = Template("""\ Remote IP: $remote_ip Tunnel Type: $tunnel_type Local IP: $local_ip Use SSL cert: $use_ssl_cert My cert: $certificate My key: $private_key His cert: $peer_cert PSK: $psk """) unixctl_status_tmpl = Template("""\ Ofport: $ofport CFM state: $cfm_state """) def __init__(self, name, row): self.name = name # 'name' should not change because it is key self.version = 0 # this is tunnel OVSDB configuration version self.last_refreshed_version = -1 self.state = "INIT" self.conf = {} self.status = {} self.cert_file = None self.update_conf(row) def update_conf(self, row): """This function updates IPsec tunnel configuration by taking 'row' from OVSDB interface table as source. If configuration changed in OVSDB then this function returns True. Otherwise, it returls False""" ret = False options = row.options use_ssl_cert = options.get("use_ssl_cert") == "true" cert = options.get("certificate") key = options.get("private_key") if use_ssl_cert: #Override with SSL certs if told so (cert, key) = (keyer.public_cert, keyer.private_key) new_conf = { "ifname" : self.name, "tunnel_type" : row.type, "remote_ip" : options.get("remote_ip"), "local_ip" : options.get("local_ip", "0.0.0.0"), "certificate" : cert, "private_key" : key, "use_ssl_cert" : use_ssl_cert, "peer_cert" : options.get("peer_cert"), "psk" : options.get("psk")} if self.conf != new_conf: # Configuration was updated in OVSDB. Validate it and figure # out what to do next with this IPsec tunnel. Also increment # version number for this IPsec tunnel so that we could tell # apart old and new tunnles in "ipsec status" output self.version += 1 ret = True self.conf = new_conf if self._validate_conf(): self.state = "CONFIGURED" else: vlog.warn("%s contains invalid configuration%s" % (self.name, self.invalid_reason)) self.state = "INVALID" new_status = { "cfm_state" : "Up" if row.cfm_fault == [False] else "Down" if row.cfm_fault == [True] else "Disabled", "ofport" : "Not assigned" if (row.ofport in [[], [-1]]) else row.ofport[0]} if self.status != new_status: # Tunnel has become unhealthy or ofport changed. Simply log this. vlog.dbg("%s changed status from %s to %s" % (self.name, str(self.status), str(new_status))) self.status = new_status return ret def mark_for_removal(self): """This function marks tunnel for removal. We can't delete tunnel right away because we have to clean any files created for this tunnel from it's run() function""" self.version += 1 self.state = "REMOVED" def run(self): if self.last_refreshed_version == self.version: # Configuration hasn't changed. Exit early. return False if self.state == "CONFIGURED": # If configuration changed then cleanup the old state # and push the new state self._cleanup_old_state() self._push_new_state() elif self.state == "INVALID": # If configuration is invalid then we withhold this # tunnel from having any configuration on the system self._cleanup_old_state() elif self.state == "REMOVED": self._cleanup_old_state() else: vlog.fatal("%s is in unexpected state" % self.name) self.last_refreshed_version = self.version return True def write_config(self, secrets, conf): if self.conf["psk"]: secrets.write("%s : PSK %s\n" % (self.conf["remote_ip"], self.conf["psk"])) auth_section = self.auth_tmpl["psk"].substitute(self.conf) else: secrets.write("%s : RSA %s\n" % (self.conf["remote_ip"], self.conf["private_key"])) auth_section = self.auth_tmpl["rsa"].substitute(self.conf) vals = self.conf.copy() vals["auth_section"] = auth_section vals["version"] = self.version conf.write(self.transp_tmpl[self.conf["tunnel_type"]].substitute(vals)) def show(self, policies, securities, conns): state = self.state if self.state == "INVALID": state += self.invalid_reason header = "Interface name: %s v%u (%s)\n" % (self.name, self.version, state) conf = self.unixctl_config_tmpl.substitute(self.conf) status = self.unixctl_status_tmpl.substitute(self.status) spds = "Kernel policies installed:\n" remote_ip = self.conf["remote_ip"] if remote_ip in policies: for line in policies[remote_ip]: spds += " " + line + "\n" sas = "Kernel security associations installed:\n" if remote_ip in securities: for line in securities[remote_ip]: sas += " " + line + "\n" cons = "Strongswan connections that are active:\n" if self.name in conns: for tname in conns[self.name]: cons += " " + conns[self.name][tname] + "\n" return header + conf + status + spds + sas + cons + "\n" def _validate_conf(self): """This function verifies if IPsec tunnel has valid configuration set in 'conf'. If it is valid, then it returns True. Otherwise, it returns False and sets the reason why configuration was considered as invalid. This function could be improved in future to also verify validness of certificates themselves so that ovs-monitor-ipsec would not pass malformed configuration to strongSwan.""" self.invalid_reason = None if not self.conf["remote_ip"]: self.invalid_reason = ": 'remote_ip' is not set" elif self.conf["peer_cert"]: if self.conf["psk"]: self.invalid_reason = ": 'psk' must be unset with PKI" elif not self.conf["certificate"]: self.invalid_reason = ": must set 'certificate' with PKI" elif not self.conf["private_key"]: self.invalid_reason = ": must set 'private_key' with PKI" elif self.conf["psk"]: if self.conf["certificate"] or self.conf["private_key"]: self.invalid_reason = ": 'certificate', 'private_key' and "\ "'use_ssl_cert' must be unset with PSK" else: self.invalid_reason = ": must set either 'psk' or 'peer_cert'" if self.invalid_reason: return False return True def _cleanup_old_state(self): if self.cert_file: try: os.remove(self.cert_file) except OSError: vlog.warn("could not remove '%s'" % (self.cert_file)); self.cert_file = None def _push_new_state(self): if self.conf["peer_cert"]: fn = keyer.CERT_DIR + "/ovs-%s.pem" % (self.conf["remote_ip"]) try: cert = open(fn, "w") try: cert.write(self.conf["peer_cert"]) finally: cert.close() vlog.info("created peer certificate %s" % (fn)) self.cert_file = fn except (OSError, IOError) as e: vlog.err("could not create certificate '%s'" % (fn)) class StrongSwanKeyer(object): STRONGSWAN_CONF = """%s charon { plugins { kernel-netlink { set_proto_port_transport_sa = yes xfrm_ack_expires = 10 } gcm { load = yes } } load_modular = yes } """ % (FILE_HEADER) IPSEC_MARK = "1/1" CONF_HEADER = """%s config setup uniqueids=no conn %%default keyingtries=%%forever type=transport keyexchange=ikev2 auto=route mark_in=%s ike=aes128gcm12-aesxcbc-modp1024 esp=aes128gcm12-modp1024 mobike=no """ % (FILE_HEADER, IPSEC_MARK) def __init__(self, strongswan_root_prefix): self.private_key = None self.public_cert = None self.tunnels = {} self.CERT_DIR = strongswan_root_prefix + "/etc/ipsec.d/certs" self.CHARON_CONF = strongswan_root_prefix + "/etc/strongswan.d/ovs.conf" self.IPSEC = strongswan_root_prefix + "/usr/sbin/ipsec" self.IPSEC_CONF = strongswan_root_prefix + "/etc/ipsec.conf" self.IPSEC_SECRETS = strongswan_root_prefix + "/etc/ipsec.secrets" def restart(self): self._initial_configuration() vlog.info("restarting strongSwan") subprocess.call([self.IPSEC, "restart"]) def is_tunneling_type_supported(self, tunnel_type): return tunnel_type in StrongSwanTunnel.transp_tmpl def add_tunnel(self, name, row): vlog.info("Tunnel %s appeared in OVSDB" % (name)) self.tunnels[name] = StrongSwanTunnel(name, row) def update_tunnel(self, name, row): t = self.tunnels[name] if t.update_conf(row): vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" % (t.name, t.version)) def del_tunnel(self, name): vlog.info("Tunnel %s disappeared from OVSDB" % (name)) self.tunnels[name].mark_for_removal() def update_ssl_credentials(self, credentials): self.public_cert = credentials[0] self.private_key = credentials[1] def show(self, unix_conn, policies, securities): """This function prints all tunnel state in 'unix_conn'. It uses 'policies' and securities' received from Linux Kernel to show if tunnels were actually configured by strongSwan.""" if not self.tunnels: unix_conn.reply("No tunnels configured with IPsec") return s = "" conns = self._get_strongswan_conns() for name, tunnel in self.tunnels.iteritems(): s += tunnel.show(policies, securities, conns) unix_conn.reply(s) def run(self): """This function runs state machine that represents overall strongSwan configuration (i.e. individual tunnel states). It creates configuration files and tells strongSwan to update configuration.""" needs_refresh = False removed_tunnels = [] ipsec_secrets = open(self.IPSEC_SECRETS, "w") ipsec_conf = open(self.IPSEC_CONF, "w") ipsec_secrets.write(FILE_HEADER) ipsec_conf.write(self.CONF_HEADER) for name, tunnel in self.tunnels.iteritems(): if tunnel.run(): needs_refresh = True if tunnel.state == "REMOVED": removed_tunnels.append(name) elif tunnel.state == "CONFIGURED": tunnel.write_config(ipsec_secrets, ipsec_conf) ipsec_secrets.close() ipsec_conf.close() for name in removed_tunnels: del self.tunnels[name] if needs_refresh: self._refresh() def _refresh(self): """This functions refreshes strongSwan configuration. Behind the scenes this function calls: 1. once "ipsec update" command that tells strongSwan to load all new tunnels from "ipsec.conf"; and 2. once "ipsec rereadsecrets" command that tells strongswan to load secrets from "ipsec.conf" file 3. for every removed tunnel "ipsec stroke down-nb <tunnel>" command that removes old tunnels. """ vlog.info("Refreshing strongSwan configuration") subprocess.call([self.IPSEC, "update"]); subprocess.call([self.IPSEC, "rereadsecrets"]); # "ipsec update" command does not remove those tunnels that were # updated or disappeared from the ipsec.conf file. So, we have # to manually remove them by calling "ipsec stroke down-nb <tunnel>" # command. We use <version> number to tell apart tunnels that # were just updated. # "ipsec down-nb" command is designed to be non-blocking (opposed # to "ipsec down" comman). This means that we should not be concerned # about possibility of ovs-monitor-ipsec to block for each tunnel # while strongSwan sends IKE messages. conns_dict = self._get_strongswan_conns() for ifname, conns in conns_dict.iteritems(): tunnel = self.tunnels.get(ifname) for conn in conns: # IPsec "connection" names that we choose in strongswan # must start with Interface name if not conn.startswith(ifname): vlog.err("%s does not start with %s" % (conn, ifname)) continue # version number should be the first integer after # interface name in IPsec "connection" try: ver = int(re.findall(r'\d+', conn[len(ifname):])[0]) except ValueError, IndexError: vlog.err("%s does not contain version number") continue if not tunnel or tunnel.version != ver: vlog.info("%s is outdated %u" % (conn, ver)) subprocess.call([self.IPSEC, "stroke", "down-nb", conn]) def _get_strongswan_conns(self): """This function parses output from 'ipsec status' command. It returns dictionary where <key> is interface name (as in OVSDB) and <value> is another dictionary. This another dictionary uses strongSwan connection name as <key> and more detailed sample line from the parsed outpus as <value>. """ conns = {} proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE) while True: line = proc.stdout.readline().strip() if line == '': break tunnel_name = line.split(":") if len(tunnel_name) < 2: continue ifname = tunnel_name[0].split("-") if len(ifname) < 2: continue if not ifname[0] in conns: conns[ifname[0]] = {} (conns[ifname[0]])[tunnel_name[0]] = line return conns def _initial_configuration(self): """This function creates initial configuration that strongSwan should receive on startup.""" f = open(self.CHARON_CONF, "w") f.write(self.STRONGSWAN_CONF) f.close() f = open(self.IPSEC_CONF, "w") f.write(self.CONF_HEADER) f.close() f = open(self.IPSEC_SECRETS, "w") f.write(FILE_HEADER) f.close() def read_ovsdb_ssl_table(data): ssl = (None, None) for ovs_rec in data["Open_vSwitch"].rows.itervalues(): if ovs_rec.ssl: ssl = (ovs_rec.ssl[0].certificate, ovs_rec.ssl[0].private_key) break keyer.update_ssl_credentials(ssl) def read_ovsdb_interface_table(data): ifaces = set() for row in data["Interface"].rows.itervalues(): if not keyer.is_tunneling_type_supported(row.type): continue if row.name in keyer.tunnels: keyer.update_tunnel(row.name, row) else: keyer.add_tunnel(row.name, row) ifaces.add(row.name) for tunnel in keyer.tunnels.keys(): if not tunnel in ifaces: keyer.del_tunnel(tunnel) def read_ovsdb(data): """This function reads all OVSDB configuration that ovs-monitor-ipsec is interested in.""" read_ovsdb_ssl_table(data) read_ovsdb_interface_table(data) 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 keyer global xfrm xfrm = XFRM(args.root_prefix if args.root_prefix else "") keyer = StrongSwanKeyer(args.root_prefix if args.root_prefix else "") remote = args.database schema_helper = ovs.db.idl.SchemaHelper() schema_helper.register_columns("Interface", ["name", "type", "options", "cfm_fault", "ofport"]) 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("xfrm/policies", "", 0, 0, unixctl_xfrm_policies, None) ovs.unixctl.command_register("xfrm/state", "", 0, 0, unixctl_xfrm_state, None) ovs.unixctl.command_register("ipsec/status", "", 0, 0, unixctl_ipsec_status, None) ovs.unixctl.command_register("tunnels/show", "", 0, 0, unixctl_show, None) 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) seqno = idl.change_seqno # Sequence number when we last processed OVSDB keyer.restart() while True: unixctl_server.run() if exiting: break idl.run() if seqno != idl.change_seqno: read_ovsdb(idl.tables) seqno = idl.change_seqno keyer.run() poller = ovs.poller.Poller() unixctl_server.wait(poller) idl.wait(poller) poller.block() unixctl_server.close() idl.close() if __name__ == '__main__': try: main() except SystemExit: # Let system.exit() calls complete normally raise except: vlog.exception("traceback") sys.exit(ovs.daemon.RESTART_EXIT_CODE)