On Tue, 7 Aug 2018 at 09:43, Qiuyu Xiao <[email protected]> wrote: > > This patch reintroduces ovs-monitor-ipsec daemon that > was previously removed by commit 2b02d770 ("openvswitch: > Allow external IPsec tunnel management.") > > After this patch, there are no IPsec flavored tunnels anymore. > IPsec is enabled by setting up the right values in: > 1. OVSDB:Interface:options column; > 2. OVSDB:Open_vSwitch:other_config column; > 3. OpenFlow pipeline. > > GRE, VXLAN, GENEVE, and STT IPsec tunnels are supported. LibreSwan and > StrongSwan IKE daemons are supported. User can choose pre-shared key, > self-signed peer certificate, or CA-signed certificate as authentication > method. s/mehod/methods > > Signed-off-by: Qiuyu Xiao <[email protected]> > Signed-off-by: Ansis Atteka <[email protected]> > Co-authored-by: Ansis Atteka <[email protected]> > ---
I have two high level comments that we privately discussed earlier on Friday: 1. the local_ip should be wildcardable. Otherwise, if routes change, then then packets may leak out unencrypted before local_ip gets explicitly updated by administrator as well. 2. the strongSwan/Ubuntu and libreswan/Fedora compatibility issue due to integrity check. I know that this could be strongswan or libreswan bug, but perhaps we could use some alternate configuration that works? Did you find one? Other than that see small implementation details > Makefile.am | 1 + > ipsec/automake.mk | 10 + > ipsec/ovs-monitor-ipsec | 1173 +++++++++++++++++++++++++++++++++++++++ > 3 files changed, 1184 insertions(+) > create mode 100644 ipsec/automake.mk > create mode 100755 ipsec/ovs-monitor-ipsec > > diff --git a/Makefile.am b/Makefile.am > index 788972804..aeb2d108f 100644 > --- a/Makefile.am > +++ b/Makefile.am > @@ -481,6 +481,7 @@ include tests/automake.mk > include include/automake.mk > include third-party/automake.mk > include debian/automake.mk > +include ipsec/automake.mk > include vswitchd/automake.mk > include ovsdb/automake.mk > include rhel/automake.mk > diff --git a/ipsec/automake.mk b/ipsec/automake.mk > new file mode 100644 > index 000000000..1e530cb42 > --- /dev/null > +++ b/ipsec/automake.mk > @@ -0,0 +1,10 @@ > +# 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. > + > +EXTRA_DIST += \ > + ipsec/ovs-monitor-ipsec > +FLAKE8_PYFILES += ipsec/ovs-monitor-ipsec > diff --git a/ipsec/ovs-monitor-ipsec b/ipsec/ovs-monitor-ipsec > new file mode 100755 > index 000000000..163b04004 > --- /dev/null > +++ b/ipsec/ovs-monitor-ipsec > @@ -0,0 +1,1173 @@ > +#!/usr/bin/env python > +# 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 > +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" > +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} > + > +""" > +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 > + > +""" > + > + auth_tmpl = {"psk": Template("""\ > + left=$local_ip > + right=$remote_ip > + authby=psk"""), > + "pki_remote": Template("""\ > + left=$local_ip > + right=$remote_ip > + leftid=$local_name > + rightid=$remote_name > + leftcert=$certificate > + rightcert=$remote_cert"""), > + "pki_ca": Template("""\ > + left=$local_ip > + 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 start_ike_daemon(self): > + """This function starts 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("Starting StrongSwan") > + subprocess.call([self.IPSEC, "start"]) > + > + 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+).*", 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(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('%s %s : PSK "%s"\n' % > + (tunnel.conf["local_ip"], > tunnel.conf["remote_ip"], local_ip should be wildcardable, if user wants to. Otherwise, if routes change then packets may sneak out in plain. > + tunnel.conf["psk"])) > + auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf) > + else: > + self.secrets_file.write("%s %s : RSA %s\n" % > + (tunnel.conf["local_ip"], > 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) > + > + auth_tmpl = {"psk": Template("""\ > + left=$local_ip > + right=$remote_ip > + authby=secret"""), > + "pki_remote": Template("""\ > + left=$local_ip > + right=$remote_ip > + leftid=@$local_name > + rightid=@$remote_name > + leftcert="$local_name" > + rightcert="$remote_name" > + leftrsasigkey=%cert"""), > + "pki_ca": Template("""\ > + left=$local_ip > + right=$remote_ip > + leftid=@$local_name > + rightid=@$remote_name > + leftcert="$local_name" > + leftrsasigkey=%cert > + rightca=%same""")} > + > + 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 start_ike_daemon(self): > + """This function starts LibreSwan.""" > + 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("Starting LibreSwan") > + subprocess.call([self.IPSEC, "start"]) Should you start or restart here? With libreswan. is there possibility of having some stale config after restart that may need to be wiped out? > + > + 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"]: > + self._delete_local_certs_and_key(monitor.conf_in_use["pki"]) > + > + # Load new state > + if monitor.conf["pki"]["certificate"]: > + self._import_local_certs_and_key(monitor.conf["pki"]) > + > + monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"]) > + needs_refresh = True > + > + # Configure the shunt policy > + if monitor.conf["skb_mark"]: > + > self.conf_file.write(SHUNT_POLICY.format(monitor.conf["skb_mark"])) > + > + if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]: > + 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('%s %s : PSK "%s"\n' % > + (tunnel.conf["local_ip"], > 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._import_remote_cert(tunnel.conf["remote_cert"], > + tunnel.conf["remote_name"]) > + 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"]: > + self._delete_remote_cert(tunnel.conf["remote_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 > + conn_in = "%s-in-%s" % (name, ver) > + conn_out = "%s-out-%s" % (name, ver) > + > + # 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_in], > + 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 > + > + while True: > + proc = subprocess.Popen([self.IPSEC, "auto", "--start", > + "--asynchronous", conn_out], > + 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 > + > + # Activate shunt policy if configured > + if monitor.conf["skb_mark"]: Should you call these commands only if skb_mark changed? Also, I am surprised you need to invoke them in the first place - would adding auto=start for libreswan shunt policies make this happen automatically(just like with strongSwan)? Maybe libreswan does not support that? > + subprocess.call([self.IPSEC, "auto", "--start", > + "--asynchronous", "prevent_unencrypted_gre"]) > + subprocess.call([self.IPSEC, "auto", "--start", > + "--asynchronous", "prevent_unencrypted_geneve"]) > + subprocess.call([self.IPSEC, "auto", "--start", > + "--asynchronous", "prevent_unencrypted_stt"]) > + subprocess.call([self.IPSEC, "auto", "--start", > + "--asynchronous", "prevent_unencrypted_vxlan"]) > + > + 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+)", conn) > + if not m: > + continue > + > + ifname = m.group(1) > + if ifname not in conns: > + conns[ifname] = {} > + (conns[ifname])[conn] = line > + > + return conns > + > + def _delete_remote_cert(self, remote_name): > + """Delete remote certiticate from the NSS database.""" > + try: > + proc = subprocess.Popen(['certutil', '-D', '-d', > + 'sql:/etc/ipsec.d/', '-n', remote_name], > + stdout=subprocess.PIPE, > + stderr=subprocess.PIPE) > + proc.wait() > + if proc.returncode: > + raise Exception(proc.stderr.read()) > + except Exception as e: > + vlog.err("Delete remote certificate failed.\n" + str(e)) s/Delete remote certificate failed/Failed to delete remote ceretificate XXX from NSS: +str(e) > + > + def _import_remote_cert(self, cert, remote_name): > + """Import remote certificate to the NSS database.""" > + try: > + proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert, > + '-d', 'sql:/etc/ipsec.d/', '-n', > + remote_name, '-t', 'P,P,P'], > + stdout=subprocess.PIPE, > + stderr=subprocess.PIPE) > + proc.wait() > + if proc.returncode: > + raise Exception(proc.stderr.read()) > + except Exception as e: > + vlog.err("Import remote certificate failed.\n" + str(e)) I would write "Failed to import remote certificate XXX into NSS:" + str(e) > + > + def _delete_local_certs_and_key(self, pki): > + """Delete certs and key from the NSS database.""" > + name = pki["local_name"] > + cacert = pki["ca_cert"] > + > + 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()) > + > + if cacert: > + proc = subprocess.Popen(['certutil', '-D', '-d', > + 'sql:/etc/ipsec.d/', '-n', 'cacert'], > + stdout=subprocess.PIPE, > + stderr=subprocess.PIPE) > + proc.wait() Sorry, may have forgotten python, but if in the next line you invoke .wait(), is there a reason you can't merge this into .call()? Same elsewhere. Also, can these commands block for long time? > + if proc.returncode: > + raise Exception(proc.stderr.read()) > + except Exception as e: > + vlog.err("Delete certs and key failed.\n" + str(e)) > + > + def _import_local_certs_and_key(self, pki): > + """Import certs and key to the NSS database.""" > + cert = pki["certificate"] > + key = pki["private_key"] > + name = pki["local_name"] > + cacert = pki["ca_cert"] > + > + try: > + # Create p12 file from pem files > + proc = subprocess.Popen(['openssl', 'pkcs12', '-export', > + '-in', cert, '-inkey', key, '-out', > + '/tmp/%s.p12' % name, '-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', '/tmp/%s.p12' % name, > + '-d', 'sql:/etc/ipsec.d/', '-W', ''], > + stdout=subprocess.PIPE, > + stderr=subprocess.PIPE) > + proc.wait() > + if proc.returncode: > + raise Exception(proc.stderr.read()) > + > + if cacert: > + proc = subprocess.Popen(['certutil', '-A', '-a', '-i', > + cacert, '-d', 'sql:/etc/ipsec.d/', > + '-n', 'cacert', '-t', 'CT,,'], > + 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)) > + subprocess.call(['rm', '-f', '/tmp/%s.p12' % name]) I would use os.remove(). Also is it possible for name to contain "../.." that would allow to navigate outside /tmp and delete other files? > + > + > +class IPsecTunnel(object): > + """This is the base class for IPsec tunnel.""" > + > + unixctl_config_tmpl = Template("""\ > + Tunnel Type: $tunnel_type > + Local IP: $local_ip > + 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"), > + "local_ip": options.get("local_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 not self.conf["local_ip"]: Can we make local_ip wilcardable again? > + self.invalid_reason = ": 'local_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 > + elif self.conf["remote_name"]: I am a little bit confused about this remote_name check that must evaluate to true here. Does this mean that no authentication method was specified or that remote_name could not be retrieved (at least to me the invalid_reason seems incorrectly set without giving hint to user what is missing)? > + if not self.conf["certificate"]: > + self.invalid_reason = ": must set 'certificate' with PKI" > + return False > + elif not self.conf["private_key"]: > + self.invalid_reason = ": must set 'private_key' with PKI" > + return False > + if not self.conf["remote_cert"] and not self.conf["ca_cert"]: > + self.invalid_reason = ": must set 'remote_cert' or 'ca_cert'" > + 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): > + 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 An easier method to pick right IKE driver would be to pass a specifc, hardcoded argument to ovs-monitor-ipsec daemon from systemd unit file or debian init.d script. Althugh, that would prevent to easily switch between libreswan on Ubuntu or strongSwan on Fedora. Here are the concerns I have with the current method you are proposing: 1. on Ubuntu you terminate ovs-monitor-ipsec if it could not find ipsec utility; and the would let the monitor process to restart it in loop 2. on Fedora you simply give up (inconsistent wrt Ubuntu) 3. if `ipsec` changes STDOUT format then ovs-monitor-ipsec may not work due to this small detail. > + try: > + proc = subprocess.Popen([self.IPSEC, "--version"], > + stdout=subprocess.PIPE) > + line = proc.stdout.readline().strip().split(" ") > + > + if len(line) >= 2 and line[1] in ["strongSwan", "Libreswan"]: > + if line[1] == "strongSwan": > + self.ike_helper = StrongSwanHelper(root_prefix) > + else: > + self.ike_helper = LibreSwanHelper(root_prefix) > + else: > + raise Exception("IKE daemon is not installed. Please install > " > + "the supported daemon (StrongSwan or > LibreSwan).") > + except Exception as e: > + vlog.err(str(e)) I think in few cases this would cause unhandled exceptions to be dumped in log file, no? > + sys.exit(1) > + > + self.ike_helper.start_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""" > + # Update PKI certs and key I think the # comment is redundant > + 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).") > + > + 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) > + > + 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.18.0 > _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
