From: Dave Tucker <d...@dtucker.co.uk> This code is used from the OVS tree and is not required for compilation. It should be removed so that no-one attempts to modify it.
Submitted-at: https://github.com/ovn-org/ovn/pull/38 Signed-off-by: Dave Tucker <d...@dtucker.co.uk> Signed-off-by: Numan Siddique <num...@ovn.org> --- Makefile.am | 1 - ipsec/.gitignore | 1 - ipsec/automake.mk | 11 - ipsec/ovs-monitor-ipsec.in | 1235 ------------------------------------ 4 files changed, 1248 deletions(-) delete mode 100644 ipsec/.gitignore delete mode 100644 ipsec/automake.mk delete mode 100755 ipsec/ovs-monitor-ipsec.in diff --git a/Makefile.am b/Makefile.am index fbd4638a1..b75c12eff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -497,7 +497,6 @@ include include/automake.mk include third-party/automake.mk include debian/automake.mk include lib/ovsdb_automake.mk -include ipsec/automake.mk include rhel/automake.mk include tutorial/automake.mk include selinux/automake.mk diff --git a/ipsec/.gitignore b/ipsec/.gitignore deleted file mode 100644 index e4083913f..000000000 --- a/ipsec/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ovs-monitor-ipsec diff --git a/ipsec/automake.mk b/ipsec/automake.mk deleted file mode 100644 index b157e7b7f..000000000 --- a/ipsec/automake.mk +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (C) 2017 Nicira, Inc. -# -# Copying and distribution of this file, with or without modification, -# are permitted in any medium without royalty provided the copyright -# notice and this notice are preserved. This file is offered as-is, -# without warranty of any kind. - -scripts_SCRIPTS += ipsec/ovs-monitor-ipsec -EXTRA_DIST += ipsec/ovs-monitor-ipsec.in -FLAKE8_PYFILES += ipsec/ovs-monitor-ipsec.in -CLEANFILES += ipsec/ovs-monitor-ipsec diff --git a/ipsec/ovs-monitor-ipsec.in b/ipsec/ovs-monitor-ipsec.in deleted file mode 100755 index 37e370324..000000000 --- a/ipsec/ovs-monitor-ipsec.in +++ /dev/null @@ -1,1235 +0,0 @@ -#! @PYTHON3@ -# Copyright (c) 2017 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. - -import argparse -import re -import subprocess -import sys -import copy -import os -from string import Template - -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" -transp_tmpl = {"gre": Template("""\ -conn $ifname-$version -$auth_section - leftprotoport=gre - rightprotoport=gre - -"""), "gre64": Template("""\ -conn $ifname-$version -$auth_section - leftprotoport=gre - rightprotoport=gre - -"""), "geneve": Template("""\ -conn $ifname-in-$version -$auth_section - leftprotoport=udp/6081 - rightprotoport=udp - -conn $ifname-out-$version -$auth_section - leftprotoport=udp - rightprotoport=udp/6081 - -"""), "stt": Template("""\ -conn $ifname-in-$version -$auth_section - leftprotoport=tcp/7471 - rightprotoport=tcp - -conn $ifname-out-$version -$auth_section - leftprotoport=tcp - rightprotoport=tcp/7471 - -"""), "vxlan": Template("""\ -conn $ifname-in-$version -$auth_section - leftprotoport=udp/4789 - rightprotoport=udp - -conn $ifname-out-$version -$auth_section - leftprotoport=udp - rightprotoport=udp/4789 - -""")} -vlog = ovs.vlog.Vlog("ovs-monitor-ipsec") -exiting = False -monitor = None -xfrm = None - - -class XFRM(object): - """This class is a simple wrapper around ip-xfrm (8) command line - utility. We are using this class only for informational purposes - so that ovs-monitor-ipsec could verify that IKE keying daemon has - installed IPsec policies and security associations into kernel as - expected.""" - - 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 dst not in policies: - policies[dst] = [] - policies[dst].append(line) - src = (a[3].split("/"))[0] - if src not 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 remote_ip not in securities: - securities[remote_ip] = [] - securities[remote_ip].append(line) - if local_ip not in securities: - securities[local_ip] = [] - securities[local_ip].append(line) - return securities - - -class StrongSwanHelper(object): - """This class does StrongSwan specific configurations.""" - - STRONGSWAN_CONF = """%s -charon.plugins.kernel-netlink.set_proto_port_transport_sa = yes -charon.plugins.kernel-netlink.xfrm_ack_expires = 10 -charon.load_modular = yes -charon.plugins.gcm.load = yes -""" % (FILE_HEADER) - - CONF_HEADER = """%s -config setup - uniqueids=yes - -conn %%default - keyingtries=%%forever - type=transport - keyexchange=ikev2 - auto=route - ike=aes256gcm16-sha256-modp2048 - esp=aes256gcm16-modp2048 - -""" % (FILE_HEADER) - - CA_SECTION = """ca ca_auth - cacert=%s - -""" - - SHUNT_POLICY = """conn prevent_unencrypted_gre - type=drop - leftprotoport=gre - mark={0} - -conn prevent_unencrypted_geneve - type=drop - leftprotoport=udp/6081 - mark={0} - -conn prevent_unencrypted_stt - type=drop - leftprotoport=tcp/7471 - mark={0} - -conn prevent_unencrypted_vxlan - type=drop - leftprotoport=udp/4789 - mark={0} - -""" - - auth_tmpl = {"psk": Template("""\ - left=0.0.0.0 - right=$remote_ip - authby=psk"""), - "pki_remote": Template("""\ - left=0.0.0.0 - right=$remote_ip - leftid=$local_name - rightid=$remote_name - leftcert=$certificate - rightcert=$remote_cert"""), - "pki_ca": Template("""\ - left=0.0.0.0 - right=$remote_ip - leftid=$local_name - rightid=$remote_name - leftcert=$certificate""")} - - def __init__(self, root_prefix): - self.CHARON_CONF = root_prefix + "/etc/strongswan.d/ovs.conf" - self.IPSEC = root_prefix + "/usr/sbin/ipsec" - self.IPSEC_CONF = root_prefix + "/etc/ipsec.conf" - self.IPSEC_SECRETS = root_prefix + "/etc/ipsec.secrets" - self.conf_file = None - self.secrets_file = None - - def restart_ike_daemon(self): - """This function restarts StrongSwan.""" - 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() - - vlog.info("Restarting StrongSwan") - subprocess.call([self.IPSEC, "restart"]) - - def get_active_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 - m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+).*", tunnel_name[0]) - if not m: - continue - ifname = m.group(1) - if ifname not in conns: - conns[ifname] = {} - (conns[ifname])[tunnel_name[0]] = line - - return conns - - def config_init(self): - self.conf_file = open(self.IPSEC_CONF, "w") - self.secrets_file = open(self.IPSEC_SECRETS, "w") - self.conf_file.write(self.CONF_HEADER) - self.secrets_file.write(FILE_HEADER) - - def config_global(self, monitor): - """Configure the global state of IPsec tunnels.""" - needs_refresh = False - - if monitor.conf_in_use != monitor.conf: - monitor.conf_in_use = copy.deepcopy(monitor.conf) - needs_refresh = True - - # Configure the shunt policy - if monitor.conf_in_use["skb_mark"]: - skb_mark = monitor.conf_in_use["skb_mark"] - self.conf_file.write(self.SHUNT_POLICY.format(skb_mark)) - - # Configure the CA cert - if monitor.conf_in_use["pki"]["ca_cert"]: - cacert = monitor.conf_in_use["pki"]["ca_cert"] - self.conf_file.write(self.CA_SECTION % cacert) - - return needs_refresh - - def config_tunnel(self, tunnel): - if tunnel.conf["psk"]: - self.secrets_file.write('0.0.0.0 %s : PSK "%s"\n' % - (tunnel.conf["remote_ip"], tunnel.conf["psk"])) - auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf) - else: - self.secrets_file.write("0.0.0.0 %s : RSA %s\n" % - (tunnel.conf["remote_ip"], - tunnel.conf["private_key"])) - if tunnel.conf["remote_cert"]: - tmpl = self.auth_tmpl["pki_remote"] - auth_section = tmpl.substitute(tunnel.conf) - else: - tmpl = self.auth_tmpl["pki_ca"] - auth_section = tmpl.substitute(tunnel.conf) - - vals = tunnel.conf.copy() - vals["auth_section"] = auth_section - vals["version"] = tunnel.version - conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals) - self.conf_file.write(conf_text) - - def config_fini(self): - self.secrets_file.close() - self.conf_file.close() - self.secrets_file = None - self.conf_file = None - - def refresh(self, monitor): - """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. - Once strongSwan vici bindings will be distributed with major - Linux distributions this function could be simplified.""" - 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 that 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" command). This means that we should not be concerned - # about possibility of ovs-monitor-ipsec to block for each tunnel - # while strongSwan sends IKE messages over Internet. - conns_dict = self.get_active_conns() - for ifname, conns in conns_dict.iteritems(): - tunnel = monitor.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 IndexError: - vlog.err("%s does not contain version number") - continue - except ValueError: - 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]) - - -class LibreSwanHelper(object): - """This class does LibreSwan specific configurations.""" - CONF_HEADER = """%s -config setup - uniqueids=yes - -conn %%default - keyingtries=%%forever - type=transport - auto=route - ike=aes_gcm256-sha2_256 - esp=aes_gcm256 - ikev2=insist - -""" % (FILE_HEADER) - - SHUNT_POLICY = """conn prevent_unencrypted_gre - type=drop - left=%defaultroute - leftprotoport=gre - mark={0} - -conn prevent_unencrypted_geneve - type=drop - left=%defaultroute - leftprotoport=udp/6081 - mark={0} - -conn prevent_unencrypted_stt - type=drop - left=%defaultroute - leftprotoport=tcp/7471 - mark={0} - -conn prevent_unencrypted_vxlan - type=drop - left=%defaultroute - leftprotoport=udp/4789 - mark={0} - -""" - - auth_tmpl = {"psk": Template("""\ - left=%defaultroute - right=$remote_ip - authby=secret"""), - "pki_remote": Template("""\ - left=%defaultroute - right=$remote_ip - leftid=@$local_name - rightid=@$remote_name - leftcert="$local_name" - rightcert="$remote_name" - leftrsasigkey=%cert"""), - "pki_ca": Template("""\ - left=%defaultroute - right=$remote_ip - leftid=@$local_name - rightid=@$remote_name - leftcert="ovs_certkey_$local_name" - leftrsasigkey=%cert - rightca=%same""")} - - CERT_PREFIX = "ovs_cert_" - CERTKEY_PREFIX = "ovs_certkey_" - - def __init__(self, libreswan_root_prefix): - self.IPSEC = libreswan_root_prefix + "/usr/sbin/ipsec" - self.IPSEC_CONF = libreswan_root_prefix + "/etc/ipsec.conf" - self.IPSEC_SECRETS = libreswan_root_prefix + "/etc/ipsec.secrets" - self.conf_file = None - self.secrets_file = None - - def restart_ike_daemon(self): - """This function restarts LibreSwan.""" - # Remove the stale information from the NSS database - self._nss_clear_database() - - 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() - - vlog.info("Restarting LibreSwan") - subprocess.call([self.IPSEC, "restart"]) - - def config_init(self): - self.conf_file = open(self.IPSEC_CONF, "w") - self.secrets_file = open(self.IPSEC_SECRETS, "w") - self.conf_file.write(self.CONF_HEADER) - self.secrets_file.write(FILE_HEADER) - - def config_global(self, monitor): - """Configure the global state of IPsec tunnels.""" - needs_refresh = False - - if monitor.conf_in_use["pki"] != monitor.conf["pki"]: - # Clear old state - if monitor.conf_in_use["pki"]["certificate"]: - local_name = monitor.conf_in_use["pki"]["local_name"] - self._nss_delete_cert_and_key(self.CERTKEY_PREFIX + local_name) - - if monitor.conf_in_use["pki"]["ca_cert"]: - self._nss_delete_cert(self.CERT_PREFIX + "cacert") - - # Load new state - if monitor.conf["pki"]["certificate"]: - cert = monitor.conf["pki"]["certificate"] - key = monitor.conf["pki"]["private_key"] - name = monitor.conf["pki"]["local_name"] - name = self.CERTKEY_PREFIX + name - self._nss_import_cert_and_key(cert, key, name) - - if monitor.conf["pki"]["ca_cert"]: - self._nss_import_cert(monitor.conf["pki"]["ca_cert"], - self.CERT_PREFIX + "cacert", 'CT,,') - - monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"]) - needs_refresh = True - - # Configure the shunt policy - if monitor.conf["skb_mark"]: - skb_mark = monitor.conf["skb_mark"] - self.conf_file.write(self.SHUNT_POLICY.format(skb_mark)) - - # Will update conf_in_use later in the 'refresh' method - if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]: - needs_refresh = True - - return needs_refresh - - def config_tunnel(self, tunnel): - if tunnel.conf["psk"]: - self.secrets_file.write('%%any %s : PSK "%s"\n' % - (tunnel.conf["remote_ip"], tunnel.conf["psk"])) - auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf) - elif tunnel.conf["remote_cert"]: - auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf) - self._nss_import_cert(tunnel.conf["remote_cert"], - self.CERT_PREFIX + tunnel.conf["remote_name"], - 'P,P,P') - else: - auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf) - - vals = tunnel.conf.copy() - vals["auth_section"] = auth_section - vals["version"] = tunnel.version - conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals) - self.conf_file.write(conf_text) - - def config_fini(self): - self.secrets_file.close() - self.conf_file.close() - self.secrets_file = None - self.conf_file = None - - def clear_tunnel_state(self, tunnel): - if tunnel.conf["remote_cert"]: - name = self.CERT_PREFIX + tunnel.conf["remote_name"] - self._nss_delete_cert(name) - - def refresh(self, monitor): - vlog.info("Refreshing LibreSwan configuration") - subprocess.call([self.IPSEC, "auto", "--rereadsecrets"]) - tunnels = set(monitor.tunnels.keys()) - - # Delete old connections - conns_dict = self.get_active_conns() - for ifname, conns in conns_dict.iteritems(): - tunnel = monitor.tunnels.get(ifname) - - for conn in conns: - # IPsec "connection" names 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: - vlog.err("%s does not contain version number") - continue - except 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, "auto", "--delete", conn]) - elif ifname in tunnels: - tunnels.remove(ifname) - - # Activate new connections - for name in tunnels: - ver = monitor.tunnels[name].version - - if monitor.tunnels[name].conf["tunnel_type"] == "gre": - conn = "%s-%s" % (name, ver) - self._start_ipsec_connection(conn) - else: - conn_in = "%s-in-%s" % (name, ver) - conn_out = "%s-out-%s" % (name, ver) - self._start_ipsec_connection(conn_in) - self._start_ipsec_connection(conn_out) - - # Update shunt policy if changed - if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]: - if monitor.conf["skb_mark"]: - subprocess.call([self.IPSEC, "auto", "--add", - "--asynchronous", "prevent_unencrypted_gre"]) - subprocess.call([self.IPSEC, "auto", "--add", - "--asynchronous", "prevent_unencrypted_geneve"]) - subprocess.call([self.IPSEC, "auto", "--add", - "--asynchronous", "prevent_unencrypted_stt"]) - subprocess.call([self.IPSEC, "auto", "--add", - "--asynchronous", "prevent_unencrypted_vxlan"]) - else: - subprocess.call([self.IPSEC, "auto", "--delete", - "--asynchronous", "prevent_unencrypted_gre"]) - subprocess.call([self.IPSEC, "auto", "--delete", - "--asynchronous", "prevent_unencrypted_geneve"]) - subprocess.call([self.IPSEC, "auto", "--delete", - "--asynchronous", "prevent_unencrypted_stt"]) - subprocess.call([self.IPSEC, "auto", "--delete", - "--asynchronous", "prevent_unencrypted_vxlan"]) - monitor.conf_in_use["skb_mark"] = monitor.conf["skb_mark"] - - def get_active_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 LibreSwan 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 - - m = re.search(r"#\d+: \"(.*)\".*", line) - if not m: - continue - - conn = m.group(1) - m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+)", conn) - if not m: - continue - - ifname = m.group(1) - if ifname not in conns: - conns[ifname] = {} - (conns[ifname])[conn] = line - - return conns - - def _start_ipsec_connection(self, conn): - # In a corner case, LibreSwan daemon restarts for some reason and - # the "ipsec auto --start" command is lost. Just retry to make sure - # the command is received by LibreSwan. - while True: - proc = subprocess.Popen([self.IPSEC, "auto", "--start", - "--asynchronous", conn], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - perr = str(proc.stderr.read()) - pout = str(proc.stdout.read()) - if not re.match(r".*Connection refused.*", perr) and \ - not re.match(r".*need --listen.*", pout): - break - - def _nss_clear_database(self): - """Remove all OVS IPsec related state from the NSS database""" - try: - proc = subprocess.Popen(['certutil', '-L', '-d', - 'sql:/etc/ipsec.d/'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - lines = proc.stdout.readlines() - - for line in lines: - s = line.strip().split() - if len(s) < 1: - continue - name = s[0] - if name.startswith(self.CERT_PREFIX): - self._nss_delete_cert(name) - elif name.startswith(self.CERTKEY_PREFIX): - self._nss_delete_cert_and_key(name) - - except Exception as e: - vlog.err("Failed to clear NSS database.\n" + str(e)) - - def _nss_import_cert(self, cert, name, cert_type): - """Cert_type is 'CT,,' for the CA certificate and 'P,P,P' for the - normal certificate.""" - try: - proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert, - '-d', 'sql:/etc/ipsec.d/', '-n', - name, '-t', cert_type], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - except Exception as e: - vlog.err("Failed to import ceretificate into NSS.\n" + str(e)) - - def _nss_delete_cert(self, name): - try: - proc = subprocess.Popen(['certutil', '-D', '-d', - 'sql:/etc/ipsec.d/', '-n', name], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - except Exception as e: - vlog.err("Failed to delete ceretificate from NSS.\n" + str(e)) - - def _nss_import_cert_and_key(self, cert, key, name): - try: - # Avoid deleting other files - path = os.path.abspath('/tmp/%s.p12' % name) - if not path.startswith('/tmp/'): - raise Exception("Illegal certificate name!") - - # Create p12 file from pem files - proc = subprocess.Popen(['openssl', 'pkcs12', '-export', - '-in', cert, '-inkey', key, '-out', - path, '-name', name, '-passout', 'pass:'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - - # Load p12 file to the database - proc = subprocess.Popen(['pk12util', '-i', path, '-d', - 'sql:/etc/ipsec.d/', '-W', ''], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - - except Exception as e: - vlog.err("Import cert and key failed.\n" + str(e)) - os.remove(path) - - def _nss_delete_cert_and_key(self, name): - try: - # Delete certificate and private key - proc = subprocess.Popen(['certutil', '-F', '-d', - 'sql:/etc/ipsec.d/', '-n', name], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - - except Exception as e: - vlog.err("Delete cert and key failed.\n" + str(e)) - - -class IPsecTunnel(object): - """This is the base class for IPsec tunnel.""" - - unixctl_config_tmpl = Template("""\ - Tunnel Type: $tunnel_type - Remote IP: $remote_ip - SKB mark: $skb_mark - Local cert: $certificate - Local name: $local_name - Local key: $private_key - Remote cert: $remote_cert - Remote name: $remote_name - CA cert: $ca_cert - PSK: $psk -""") - - unixctl_status_tmpl = Template("""\ - Ofport: $ofport - CFM state: $cfm_state -""") - - def __init__(self, name, row): - self.name = name # 'name' will not change because it is key in OVSDB - self.version = 0 # 'version' is increased on configuration changes - self.last_refreshed_version = -1 - self.state = "INIT" - self.conf = {} - self.status = {} - self.update_conf(row) - - def update_conf(self, row): - """This function updates IPsec tunnel configuration by using 'row' - from OVSDB interface table. If configuration was actually changed - in OVSDB then this function returns True. Otherwise, it returns - False.""" - ret = False - options = row.options - remote_cert = options.get("remote_cert") - remote_name = options.get("remote_name") - if remote_cert: - remote_name = monitor._get_cn_from_cert(remote_cert) - - new_conf = { - "ifname": self.name, - "tunnel_type": row.type, - "remote_ip": options.get("remote_ip"), - "skb_mark": monitor.conf["skb_mark"], - "certificate": monitor.conf["pki"]["certificate"], - "private_key": monitor.conf["pki"]["private_key"], - "ca_cert": monitor.conf["pki"]["ca_cert"], - "remote_cert": remote_cert, - "remote_name": remote_name, - "local_name": monitor.conf["pki"]["local_name"], - "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 of this IPsec tunnel so that we could tell - # apart old and new tunnels in "ipsec status" output. - self.version += 1 - ret = True - self.conf = new_conf - - if self._is_valid_tunnel_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.""" - self.version += 1 - self.state = "REMOVED" - - 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 = "IPsec 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 _is_valid_tunnel_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 IKE daemon.""" - - self.invalid_reason = None - - if not self.conf["remote_ip"]: - self.invalid_reason = ": 'remote_ip' is not set" - return False - - if self.conf["psk"]: - if self.conf["certificate"] or self.conf["private_key"] \ - or self.conf["ca_cert"] or self.conf["remote_cert"] \ - or self.conf["remote_name"]: - self.invalid_reason = ": 'certificate', 'private_key', "\ - "'ca_cert', 'remote_cert', and "\ - "'remote_name' must be unset with PSK" - return False - # If configuring authentication with CA-signed certificate or - # self-signed certificate, the 'remote_name' should be specified at - # this point. When using CA-signed certificate, the 'remote_name' is - # read from interface's options field. When using self-signed - # certificate, the 'remote_name' is extracted from the 'remote_cert' - # file. - elif self.conf["remote_name"]: - if not self.conf["certificate"]: - self.invalid_reason = ": must set 'certificate' as local"\ - " certificate when using CA-signed"\ - " certificate or self-signed"\ - " certificate to authenticate peers" - return False - elif not self.conf["private_key"]: - self.invalid_reason = ": must set 'private_key' as local"\ - " private key when using CA-signed"\ - " certificate or self-signed"\ - " certificate to authenticate peers" - return False - if not self.conf["remote_cert"] and not self.conf["ca_cert"]: - self.invalid_reason = ": must set 'remote_cert' when using"\ - " self-signed certificate"\ - " authentication or 'ca_cert' when"\ - " using CA-signed certificate"\ - " authentication" - return False - else: - self.invalid_reason = ": must choose a authentication method" - return False - - return True - - -class IPsecMonitor(object): - """This class monitors and configures IPsec tunnels""" - - def __init__(self, root_prefix, ike_daemon): - self.IPSEC = root_prefix + "/usr/sbin/ipsec" - self.tunnels = {} - - # Global configuration shared by all tunnels - self.conf = { - "pki": { - "private_key": None, - "certificate": None, - "ca_cert": None, - "local_name": None - }, - "skb_mark": None - } - self.conf_in_use = copy.deepcopy(self.conf) - - # Choose to either use StrongSwan or LibreSwan as IKE daemon - if ike_daemon == "strongswan": - self.ike_helper = StrongSwanHelper(root_prefix) - elif ike_daemon == "libreswan": - self.ike_helper = LibreSwanHelper(root_prefix) - else: - vlog.err("The IKE daemon should be strongswan or libreswan.") - sys.exit(1) - - # Check whether ipsec command is available - if not os.path.isfile(self.IPSEC) or \ - not os.access(self.IPSEC, os.X_OK): - vlog.err("IKE daemon is not installed in the system.") - - self.ike_helper.restart_ike_daemon() - - def is_tunneling_type_supported(self, tunnel_type): - """Returns True if we know how to configure IPsec for these - types of tunnels. Otherwise, returns False.""" - return tunnel_type in ["gre", "geneve", "vxlan", "stt"] - - def is_ipsec_required(self, options_column): - """Return True if tunnel needs to be encrypted. Otherwise, - returns False.""" - return "psk" in options_column or \ - "remote_name" in options_column or \ - "remote_cert" in options_column - - def add_tunnel(self, name, row): - """Adds a new tunnel that monitor will provision with 'name'.""" - vlog.info("Tunnel %s appeared in OVSDB" % (name)) - self.tunnels[name] = IPsecTunnel(name, row) - - def update_tunnel(self, name, row): - """Updates configuration of already existing tunnel with 'name'.""" - tunnel = self.tunnels[name] - if tunnel.update_conf(row): - vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" % - (tunnel.name, tunnel.version)) - - def del_tunnel(self, name): - """Deletes tunnel by 'name'.""" - vlog.info("Tunnel %s disappeared from OVSDB" % (name)) - self.tunnels[name].mark_for_removal() - - def update_conf(self, pki, skb_mark): - """Update the global configuration for IPsec tunnels""" - self.conf["pki"]["certificate"] = pki[0] - self.conf["pki"]["private_key"] = pki[1] - self.conf["pki"]["ca_cert"] = pki[2] - self.conf["pki"]["local_name"] = pki[3] - - # Update skb_mark used in IPsec policies. - self.conf["skb_mark"] = skb_mark - - def read_ovsdb_open_vswitch_table(self, data): - """This functions reads IPsec relevant configuration from Open_vSwitch - table.""" - pki = [None, None, None, None] - skb_mark = None - is_valid = False - - for row in data["Open_vSwitch"].rows.itervalues(): - pki[0] = row.other_config.get("certificate") - pki[1] = row.other_config.get("private_key") - pki[2] = row.other_config.get("ca_cert") - skb_mark = row.other_config.get("ipsec_skb_mark") - - # Test whether it's a valid configration - if pki[0] and pki[1]: - pki[3] = self._get_cn_from_cert(pki[0]) - if pki[3]: - is_valid = True - elif not pki[0] and not pki[1] and not pki[2]: - is_valid = True - - if not is_valid: - vlog.warn("The cert and key configuration is not valid. " - "The valid configuations are 1): certificate, private_key " - "and ca_cert are not set; or 2): certificate and " - "private_key are all set.") - else: - self.update_conf(pki, skb_mark) - - def read_ovsdb_interface_table(self, data): - """This function reads the IPsec relevant configuration from Interface - table.""" - ifaces = set() - - for row in data["Interface"].rows.itervalues(): - if not self.is_tunneling_type_supported(row.type): - continue - if not self.is_ipsec_required(row.options): - continue - if row.name in self.tunnels: - self.update_tunnel(row.name, row) - else: - self.add_tunnel(row.name, row) - ifaces.add(row.name) - - # Mark for removal those tunnels that just disappeared from OVSDB - for tunnel in self.tunnels.keys(): - if tunnel not in ifaces: - self.del_tunnel(tunnel) - - def read_ovsdb(self, data): - """This function reads all configuration from OVSDB that - ovs-monitor-ipsec is interested in.""" - self.read_ovsdb_open_vswitch_table(data) - self.read_ovsdb_interface_table(data) - - 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 the IKE deamon.""" - if not self.tunnels: - unix_conn.reply("No tunnels configured with IPsec") - return - s = "" - conns = self.ike_helper.get_active_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 whole - IPsec configuration (i.e. merged together from individual - tunnel state machines). It creates configuration files and - tells IKE daemon to update configuration.""" - needs_refresh = False - removed_tunnels = [] - - self.ike_helper.config_init() - - if self.ike_helper.config_global(self): - needs_refresh = True - - for name, tunnel in self.tunnels.iteritems(): - if tunnel.last_refreshed_version != tunnel.version: - tunnel.last_refreshed_version = tunnel.version - needs_refresh = True - - if tunnel.state == "REMOVED" or tunnel.state == "INVALID": - removed_tunnels.append(name) - elif tunnel.state == "CONFIGURED": - self.ike_helper.config_tunnel(self.tunnels[name]) - - self.ike_helper.config_fini() - - for name in removed_tunnels: - # LibreSwan needs to clear state from database - if hasattr(self.ike_helper, "clear_tunnel_state"): - self.ike_helper.clear_tunnel_state(self.tunnels[name]) - del self.tunnels[name] - - if needs_refresh: - self.ike_helper.refresh(self) - - def _get_cn_from_cert(self, cert): - try: - proc = subprocess.Popen(['openssl', 'x509', '-noout', '-subject', - '-nameopt', 'RFC2253', '-in', cert], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - proc.wait() - if proc.returncode: - raise Exception(proc.stderr.read()) - m = re.search(r"CN=(.+?),", proc.stdout.readline()) - if not m: - raise Exception("No CN in the certificate subject.") - except Exception as e: - vlog.warn(str(e)) - return None - - return m.group(1) - - -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 monitor - conns = monitor.ike_helper.get_active_conns() - conn.reply(str(conns)) - - -def unixctl_show(conn, unused_argv, unused_aux): - global monitor - global xfrm - policies = xfrm.get_policies() - securities = xfrm.get_securities() - monitor.show(conn, policies, securities) - - -def unixctl_refresh(conn, unused_argv, unused_aux): - global monitor - monitor.ike_helper.refresh(monitor) - conn.reply(None) - - -def unixctl_exit(conn, unused_argv, unused_aux): - global monitor - global exiting - exiting = True - - # Make sure persistent global states are cleared - monitor.update_conf([None, None, None, None], None) - # Make sure persistent tunnel states are cleared - for tunnel in monitor.tunnels.keys(): - monitor.del_tunnel(tunnel) - monitor.run() - - conn.reply(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).") - parser.add_argument("--ike-daemon", metavar="IKE-DAEMON", - help="The IKE daemon used for IPsec tunnels" - " (either libreswan or strongswan).") - - 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 monitor - global xfrm - - root_prefix = args.root_prefix if args.root_prefix else "" - xfrm = XFRM(root_prefix) - monitor = IPsecMonitor(root_prefix, args.ike_daemon) - - 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", ["other_config"]) - 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("refresh", "", 0, 0, unixctl_refresh, 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) - - # Sequence number when OVSDB was processed last time - seqno = idl.change_seqno - - while True: - unixctl_server.run() - if exiting: - break - - idl.run() - if seqno != idl.change_seqno: - monitor.read_ovsdb(idl.tables) - seqno = idl.change_seqno - - monitor.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) -- 2.26.2 _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev