summaryrefslogtreecommitdiff
path: root/ipsec
diff options
context:
space:
mode:
authorQiuyu Xiao <qiuyu.xiao.qyx@gmail.com>2018-09-19 17:15:54 -0400
committerBen Pfaff <blp@ovn.org>2018-11-09 14:45:11 -0800
commit22c5eafb6efa874014a5234de8ca587b693db4df (patch)
tree1c3093fad00673c4c1015a4a3b03c60d1c84a3cb /ipsec
parentd96d0b019a6557d41d11af48925da840b3a1ecb6 (diff)
downloadopenvswitch-22c5eafb6efa874014a5234de8ca587b693db4df.tar.gz
ipsec: reintroduce IPsec support for tunneling
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 methods. Signed-off-by: Qiuyu Xiao <qiuyu.xiao.qyx@gmail.com> Signed-off-by: Ansis Atteka <aatteka@ovn.org> Co-authored-by: Ansis Atteka <aatteka@ovn.org> Signed-off-by: Ben Pfaff <blp@ovn.org>
Diffstat (limited to 'ipsec')
-rw-r--r--ipsec/automake.mk10
-rwxr-xr-xipsec/ovs-monitor-ipsec1235
2 files changed, 1245 insertions, 0 deletions
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..c93081d32
--- /dev/null
+++ b/ipsec/ovs-monitor-ipsec
@@ -0,0 +1,1235 @@
+#!/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
+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)