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. Signed-off-by: Ansis Atteka <[email protected]> Signed-off-by: Qiuyu Xiao <[email protected]> --- Documentation/automake.mk | 1 + Documentation/howto/index.rst | 1 + Documentation/howto/ipsec.rst | 256 +++++++ Makefile.am | 1 + debian/automake.mk | 3 + debian/control | 21 + debian/openvswitch-ipsec.dirs | 1 + debian/openvswitch-ipsec.init | 189 +++++ debian/openvswitch-ipsec.install | 1 + ipsec/automake.mk | 10 + ipsec/ovs-monitor-ipsec | 1152 ++++++++++++++++++++++++++++++ vswitchd/vswitch.xml | 124 +++- 12 files changed, 1751 insertions(+), 9 deletions(-) create mode 100644 Documentation/howto/ipsec.rst create mode 100644 debian/openvswitch-ipsec.dirs create mode 100644 debian/openvswitch-ipsec.init create mode 100644 debian/openvswitch-ipsec.install create mode 100644 ipsec/automake.mk create mode 100755 ipsec/ovs-monitor-ipsec diff --git a/Documentation/automake.mk b/Documentation/automake.mk index 2202df45b..3a505924a 100644 --- a/Documentation/automake.mk +++ b/Documentation/automake.mk @@ -59,6 +59,7 @@ DOC_SOURCE = \ Documentation/howto/docker.rst \ Documentation/howto/dpdk.rst \ Documentation/howto/firewalld.rst \ + Documentation/howto/ipsec.rst \ Documentation/howto/kvm.rst \ Documentation/howto/libvirt.rst \ Documentation/howto/selinux.rst \ diff --git a/Documentation/howto/index.rst b/Documentation/howto/index.rst index 201d6936b..9a3487be3 100644 --- a/Documentation/howto/index.rst +++ b/Documentation/howto/index.rst @@ -37,6 +37,7 @@ OVS :maxdepth: 2 kvm + ipsec selinux libvirt ssl diff --git a/Documentation/howto/ipsec.rst b/Documentation/howto/ipsec.rst new file mode 100644 index 000000000..67f0f4a00 --- /dev/null +++ b/Documentation/howto/ipsec.rst @@ -0,0 +1,256 @@ +.. + 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. + + Convention for heading levels in Open vSwitch documentation: + + ======= Heading 0 (reserved for the title in a document) + ------- Heading 1 + ~~~~~~~ Heading 2 + +++++++ Heading 3 + ''''''' Heading 4 + + Avoid deeper levels because they do not render well. + +======================================= +Encrypt Open vSwitch Tunnels with IPsec +======================================= + +This document describes how to use Open vSwitch to provide IPsec security for +STT, GENEVE, GRE and VXLAN tunnels. This document assumes that you have +already installed Open vSwitch. + +Setup +----- + +Install strongSwan and openvswitch-ipsec debian packages:: + + $ apt-get install strongswan + $ dpkg -i openvswitch-ipsec_<version>_amd64.deb + + +Configuration +------------- + +The IPsec configuration is done by setting options of the tunnel interface. +ovs-monitor-ipsec configures IKE daemom accordingly based on the tunnel options. + +Authentication Methods +~~~~~~~~~~~~~~~~~~~~~~ + +Hosts of the IPsec tunnel need to authenticate each other to build a secure +channel. There are three authentication methods: + +1) You can set a pre-shared key in both hosts to do authentication. This + method is easier to use but less secure:: + + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:local_ip=1.1.1.1 \ + options:remote_ip=2.2.2.2 \ + options:psk=swordfish]) + +2) You can use the certificate of remote host to do authentication. First, + generate certificate and private key in each host. The certificate could be + self-signed. Refer to the ovs-pki(8) man page for more information regarding + certificate and key generation. Then, copy the remote certificate to the local + host and type:: + + % ovs-vsctl set Open_vSwitch . \ + other_config:certificate=/path/to/local_cert.pem \ + other_config:private_key=/path/to/priv_key.pem + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:local_ip=1.1.1.1 \ + options:remote_ip=2.2.2.2 \ + options:remote_cert=/path/to/remote_cert.pem + + `local_cert.pem` is the certificate of the local host. `priv_key.pem` + is the private key of the local host. `priv_key.pem` needs to be stored in + a secure location. `remote_cert.pem` is the certificate of the remote host. + +3) You can also use CA certificate to do authentication. First, you need to + establish your public key infrastructure. The certificate of each host + needs to be signed by the CA. Refer to the ovs-pki(8) man page + for more information regarding PKI establishment. Then, copy the CA + certificate to the local host and type:: + + % ovs-vsctl set Open_vSwitch . \ + other_config:certificate=/path/to/local_cert.pem \ + other_config:private_key=/path/to/priv_key.pem \ + other_config:ca_cert=/path/to/ca_cert.pem + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:local_ip=1.1.1.1 \ + options:remote_ip=2.2.2.2 \ + options:remote_name=remote_cn + + strongSwan extracts identity from the certificate's `subjectAltName` field, + so you have to use x509 v3 certificate. `remote_cn` is the hostname from the + `subjectAltName` field of the remote host's certificate. + +Forwarding Modes +~~~~~~~~~~~~~~~~ +There is delay between the user setting up IPsec tunnel and the IPsec tunnel +actually taking affect to encrypt packets. To offset the risk of unencrypted +packets leaking out during this period, you can choose a more secure forwarding +mode. There are three different forwarding modes: + +1) ovs-monitor-ipsec assumes that packets from all OVS tunnels by default + should be allowed to exit unencrypted unless particular tunnel is + explicitly configured with IPsec parameters:: + + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) + + + The problem with this mode is that there is inherent race condition + between ovs-monitor-ipsec and ovs-vswitchd daemons where ovs-vswitchd + could enable forwarding before ovs-monitor-ipsec actually had a chance + to configure IPsec policies. + This mode should be used only and only if tunnel configuration is static + and/or if there is firewall that can drop the plain packets that + occasionally leak the tunnel unencrypted on OVSDB (re)configuration + events. + +2) ovs-monitor-ipsec assumes that packets marked with a given SKB mark + must be encrypted and hence should not be allowed to leave the system + unencrypted:: + + % ovs-vsctl set Open_vSwitch . other_config:ipsec_skb_mark=1/1 + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) + + With this mode ovs-monitor-ipsec makes IKE daemon to install IPsec shunt + policies that serve as safety net and prevent unencrypted tunnel packets + to leave the host in case ovs-vswitchd configured datapath before + ovs-monitor-ipsec installed IPsec policies. + However, assumption here is that OpenFlow controller was careful + and installed OpenFlow rule with set SKB mark action specified in + OVSDB Open_vSwitch table before the first packet was able to leave + the OVS tunnel. + +3) ovs-monitor-ipsec assumes that packets coming from all OVS tunnels + by default need to be protected with IPsec unless the tunnel is explicitly + allowed to leave unencrypted. This is inverse behavior of the second + mode:: + + % ovs-vsctl set Open_vSwitch . other_config:ipsec_skb_mark=0/1 + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) + + With this solution ovs-monitor-ipsec tells IKE daemon + to install IPsec shunt policies to match on skb mark 0 + which should be the default skb mark value for all tunnel packets + going through NetFilter/XFRM hooks. As a result IPsec assumes + that all packets coming from tunnels should be encrypted unless + OpenFlow controller explicitly set skb mark to a non-zero value. + This is the most secure mode, but at the same time the most intrusive + one, because the OpenFlow pipeline needs to be overhauled just because + now IPsec is enabled. + + Note that instead of using least significant bit of SKB mark, you could + just as well have used any other bit. + + +Troubleshooting +--------------- + +Use following ovs-apptcl command to get ovs-monitor-ipsec internal +representation of tunnel configuration:: + + % ovs-appctl -t ovs-monitor-ipsec tunnels/show + +If there is misconfiguration then ovs-appctl should indicate why. +For example:: + + Interface name: gre0 v5 (CONFIGURED) <--- Should be set to CONFIGURED. + Otherwise, error message will + be provided + Tunnel Type: gre + Local IP: 1.1.1.1 + Remote IP: 2.2.2.2 + SKB mark: None + Local cert: None + Local name: None + Local key: None + Remote cert: None + Remote name: None + CA cert: None + PSK: swordfish + Ofport: 1 <--- Whether ovs-vswitchd has assigned Ofport + number to this Tunnel Port + CFM state: Up <--- Whether CFM declared this tunnel healthy + Kernel policies installed: + ... <--- IPsec policies for this OVS tunnel in + Linux Kernel installed by strongSwan + Kernel security associations installed: + ... <--- IPsec security associations for this OVS + tunnel in Linux Kernel installed by + strongswan + Strongswan connections that are active: + ... <--- strongSwan "connections" for this OVS + tunnel + +You can also check the logs of the ovs-ipsec-monitor daemon and the IKE daemon +to locate issues. + +Limitations +----------- + +There are several limitations: + +1) Some older Open vSwitch datapath kernel modules (in Linux Kernel tree) + do not support route lookups with transport L4 ports properly. In + this case Ethernet over L4 tunneling protocols (e.g. STT, GENEVE) + would not work. However, GRE would still work because it does not + have concept of L4 ports. + +2) Some strongSwan versions might not support certain features. For + example: + + a) AES GCM ciphers that improve performance. + b) xfrm_acq_expires setting in strongSwan configuration file. + This setting tells strongSwan how aggressively to retry + establishing tunnel, if peer did not respond to previous keying + request. + c) set_proto_port_transport_sa in charon configuration file that tells + Linux Kernel to filter out unexpected packets that came over IPsec + tunnel. + d) IPsec transport mode for NAT deployment. + +3) Since IPsec decryption is done from the software when packet has already + gotten past NIC hardware, then benefits of the most tunneling offloads, + like GENEVE or TSO for STT can't be leveraged unless NIC has IPsec offloads + as well and the NIC knows how to leverage them. + +Bug Reporting +------------- + +If you think you may have found a bug with security implications, like + +1) IPsec protected tunnel accepted packets that came unencrypted; OR +2) IPsec protected tunnel allowed packets to leave unencrypted; + +Then report such bugs according to :doc:`/internals/security`. + +If bug does not have security implications, then report it according to +instructions in :doc:`/internals/bugs`. + +There is also a possibility that there is a bug in strongSwan. In that +case report it to strongSwan mailing list. diff --git a/Makefile.am b/Makefile.am index e02799a90..c0fef11fd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -480,6 +480,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/debian/automake.mk b/debian/automake.mk index 4d8e204bb..8a8d43c9f 100644 --- a/debian/automake.mk +++ b/debian/automake.mk @@ -20,6 +20,9 @@ EXTRA_DIST += \ debian/openvswitch-datapath-source.copyright \ debian/openvswitch-datapath-source.dirs \ debian/openvswitch-datapath-source.install \ + debian/openvswitch-ipsec.dirs \ + debian/openvswitch-ipsec.init \ + debian/openvswitch-ipsec.install \ debian/openvswitch-pki.dirs \ debian/openvswitch-pki.postinst \ debian/openvswitch-pki.postrm \ diff --git a/debian/control b/debian/control index a4c031d85..9443e91c9 100644 --- a/debian/control +++ b/debian/control @@ -320,3 +320,24 @@ Description: Open vSwitch development package 1000V. . This package provides openvswitch headers and libopenvswitch for developers. + +Package: openvswitch-ipsec +Architecture: linux-any +Depends: iproute2, + openvswitch-common (= ${binary:Version}), + openvswitch-switch (= ${binary:Version}), + python, + python-openvswitch (= ${source:Version}), + strongswan, + ${misc:Depends}, + ${shlibs:Depends} +Description: Open vSwitch IPsec tunneling support + Open vSwitch is a production quality, multilayer, software-based, + Ethernet virtual switch. It is designed to enable massive network + automation through programmatic extension, while still supporting + standard management interfaces and protocols (e.g. NetFlow, IPFIX, + sFlow, SPAN, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed + to support distribution across multiple physical servers similar to + VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V. + . + This package provides IPsec tunneling support for OVS tunnels. diff --git a/debian/openvswitch-ipsec.dirs b/debian/openvswitch-ipsec.dirs new file mode 100644 index 000000000..fca44aa7b --- /dev/null +++ b/debian/openvswitch-ipsec.dirs @@ -0,0 +1 @@ +usr/share/openvswitch/scripts \ No newline at end of file diff --git a/debian/openvswitch-ipsec.init b/debian/openvswitch-ipsec.init new file mode 100644 index 000000000..1a9683785 --- /dev/null +++ b/debian/openvswitch-ipsec.init @@ -0,0 +1,189 @@ +#!/bin/sh +# +# Copyright (c) 2007, 2009 Javier Fernandez-Sanguino <[email protected]> +# +# This is free software; you may redistribute it and/or modify +# it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2, +# or (at your option) any later version. +# +# This is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License with +# the Debian operating system, in /usr/share/common-licenses/GPL; if +# not, write to the Free Software Foundation, Inc., 59 Temple Place, +# Suite 330, Boston, MA 02111-1307 USA +# +### BEGIN INIT INFO +# Provides: openvswitch-ipsec +# Required-Start: $network $local_fs $remote_fs openvswitch-switch +# Required-Stop: $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Open vSwitch GRE-over-IPsec daemon +# Description: The ovs-monitor-ipsec script provides support for encrypting GRE +# tunnels with IPsec. +### END INIT INFO + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +DAEMON=/usr/share/openvswitch/scripts/ovs-monitor-ipsec # Daemon's location +NAME=ovs-monitor-ipsec # Introduce the short server's name here +LOGDIR=/var/log/openvswitch # Log directory to use + +PIDFILE=/var/run/openvswitch/$NAME.pid + +test -x $DAEMON || exit 0 + +. /lib/lsb/init-functions + +DODTIME=10 # Time to wait for the server to die, in seconds + # If this value is set too low you might not + # let some servers to die gracefully and + # 'restart' will not work + +set -e + +running_pid() { +# Check if a given process pid's cmdline matches a given name + pid=$1 + name=$2 + [ -z "$pid" ] && return 1 + [ ! -d /proc/$pid ] && return 1 + cmd=`cat /proc/$pid/cmdline | tr "\000" " "|cut -d " " -f 2` + # Is this the expected server + [ "$cmd" != "$name" ] && return 1 + return 0 +} + +running() { +# Check if the process is running looking at /proc +# (works for all users) + + # No pidfile, probably no daemon present + [ ! -f "$PIDFILE" ] && return 1 + pid=`cat $PIDFILE` + running_pid $pid $DAEMON || return 1 + return 0 +} + +start_server() { + if [ ! -d /var/run/openvswitch ]; then + install -d -m 755 -o root -g root /var/run/openvswitch + fi + + /usr/share/openvswitch/scripts/ovs-monitor-ipsec \ + --pidfile=$PIDFILE --log-file --detach --monitor \ + unix:/var/run/openvswitch/db.sock + + return 0 +} + +stop_server() { + if [ -e $PIDFILE ]; then + kill `cat $PIDFILE` + fi + + return 0 +} + +force_stop() { +# Force the process to die killing it manually + [ ! -e "$PIDFILE" ] && return + if running ; then + kill -15 $pid + # Is it really dead? + sleep "$DODTIME" + if running ; then + kill -9 $pid + sleep "$DODTIME" + if running ; then + echo "Cannot kill $NAME (pid=$pid)!" + exit 1 + fi + fi + fi + rm -f $PIDFILE +} + + +case "$1" in + start) + log_daemon_msg "Starting $NAME" + # Check if it's running first + if running ; then + log_progress_msg "apparently already running" + log_end_msg 0 + exit 0 + fi + if start_server && running ; then + # It's ok, the server started and is running + log_end_msg 0 + else + # Either we could not start it or it is not running + # after we did + # NOTE: Some servers might die some time after they start, + # this code does not try to detect this and might give + # a false positive (use 'status' for that) + log_end_msg 1 + fi + ;; + stop) + log_daemon_msg "Stopping $NAME" + if running ; then + # Only stop the server if we see it running + stop_server + log_end_msg $? + else + # If it's not running don't do anything + log_progress_msg "apparently not running" + log_end_msg 0 + exit 0 + fi + ;; + force-stop) + # First try to stop gracefully the program + $0 stop + if running; then + # If it's still running try to kill it more forcefully + log_daemon_msg "Stopping (force) $NAME" + force_stop + log_end_msg $? + fi + ;; + restart|force-reload) + log_daemon_msg "Restarting $NAME" + stop_server + # Wait some sensible amount, some server need this + [ -n "$DODTIME" ] && sleep $DODTIME + start_server + running + log_end_msg $? + ;; + status) + log_daemon_msg "Checking status of $NAME" + if running ; then + log_progress_msg "running" + log_end_msg 0 + else + log_progress_msg "apparently not running" + log_end_msg 1 + exit 1 + fi + ;; + # Use this if the daemon cannot reload + reload) + log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon" + log_warning_msg "cannot re-read the config file (use restart)." + ;; + *) + N=/etc/init.d/openvswitch-ipsec + echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/debian/openvswitch-ipsec.install b/debian/openvswitch-ipsec.install new file mode 100644 index 000000000..8fe665cb3 --- /dev/null +++ b/debian/openvswitch-ipsec.install @@ -0,0 +1 @@ +ipsec/ovs-monitor-ipsec usr/share/openvswitch/scripts 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..8cb34ab9f --- /dev/null +++ b/ipsec/ovs-monitor-ipsec @@ -0,0 +1,1152 @@ +#!/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 glob +import os +import re +import subprocess +import sys +import copy +from string import Template + +from ovs.db import error +from ovs.db import types +import ovs.daemon +import ovs.db.idl +import ovs.dirs +import ovs.unixctl +import ovs.unixctl.server +import ovs.util +import ovs.vlog + + +FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n" +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 not dst in policies: + policies[dst] = [] + policies[dst].append(line) + src = (a[3].split("/"))[0] + if not src in policies: + policies[src] = [] + policies[src].append(line) + return policies + + def get_securities(self): + """This function returns IPsec security associations (from kernel) + in a dictionary where <key> is destination IPv4 address and <value> + is SELECTOR.""" + securities = {} + proc = subprocess.Popen([self.IP, 'xfrm', 'state'], + stdout=subprocess.PIPE) + while True: + line = proc.stdout.readline().strip() + if line == '': + break + a = line.split(" ") + if len(a) >= 4 and a[0] == "sel" and a[1] == "src" and a[3] == "dst": + remote_ip = a[4].rstrip().split("/")[0] + local_ip = a[2].rstrip().split("/")[0] + if not remote_ip in securities: + securities[remote_ip] = [] + securities[remote_ip].append(line) + if not local_ip in securities: + securities[local_ip] = [] + securities[local_ip].append(line) + return securities + +class 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 +""" % (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, strongswan_root_prefix): + self.CHARON_CONF = strongswan_root_prefix + "/etc/strongswan.d/ovs.conf" + self.IPSEC = strongswan_root_prefix + "/usr/sbin/ipsec" + self.IPSEC_CONF = strongswan_root_prefix + "/etc/ipsec.conf" + self.IPSEC_SECRETS = strongswan_root_prefix + "/etc/ipsec.secrets" + 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 not ifname 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"], + 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"]: + auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf) + 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 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 ValueError, IndexError: + vlog.err("%s does not contain version number") + continue + + if not tunnel or tunnel.version != ver: + vlog.info("%s is outdated %u" % (conn, ver)) + subprocess.call([self.IPSEC, "stroke", "down-nb", conn]) + +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"]) + + 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, 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"]: + 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 not ifname 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)) + + 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)) + + 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() + 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]) + +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"]: + 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"]: + 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 + 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)) + 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 + 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 not tunnel 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) + + seqno = idl.change_seqno # Sequence number when OVSDB was processed last time + + 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) diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml index b93da69bd..d2ac31cdb 100644 --- a/vswitchd/vswitch.xml +++ b/vswitchd/vswitch.xml @@ -473,10 +473,8 @@ OpenFlow specification mandates the timeout to be at least one second. The default is 10 seconds. </p> - </column> - + </column> </group> - <group title="Status"> <column name="next_cfg"> Sequence number for client to increment. When a client modifies @@ -740,6 +738,87 @@ </column> </group> + <group title="IPsec"> + <p> + The global configuration of IPsec tunnels is set in the + <code>other_config</code> column of the <code>Open_vSwitch</code> + table. The individual IPsec tunnel configuration is set in the + <code>options</code> column of the <code>Interface</code> table. + </p> + <p> + <code>ipsec_skb_mark</code> is set to choose forwarding mode which + prevents unencrypted packets being sent out since there is delay + between the user setting up IPsec tunnel and the IPsec tunnel actually + taking affect to encrypt packets. + </p> + <p> + There are three authentication modes: + </p> + <ol> + <li> + To authenticate remote switch with preshared key, + <code>private_key</code>, <code>certificate</code>, and + <code>ca_cert</code> must not be set. <code>psk</code> in the + <code>options</code> column of the <code>Interface</code> table + should be set as the preshared key. + </li> + <li> + To authenticate remote switch with certificate, + <code>private_key</code> and <code>certificate</code> must be set. In + the CA based authentication mode, <code>ca_cert</code> must be set. + <code>remote_name</code> in the <code>options</code> column of the + <code>Interface</code> table must be set as the common name (CN) of + the remote certificate. The remote certificate must be signed by the + CA. + </li> + <li> + To authenticate remote switch without CA certificate, + <code>remote_cert</code> in the <code>options</code> column of the + <code>Interface</code> table must be set. The remote certificate can + be self-signed. + </li> + </ol> + <column name="other_config" key="ipsec_skb_mark" + type='{"type": "string"}'> + <p> + Setting ipsec_skb_mark as 1/1 blocks unecrypted packets with skb + mark set as 1. Besides, the OpenFlow rule with set SKB mark action + specified in OVSDB needs to be installed in Open_vSwitch table + before the first packet was able to leave the OVS tunnel. + </p> + <p> + Setting ipsec_skb_mark as 0/1 blocks unecrypted packets without skb + mark. As a result, IPsec assumes that all packets coming from + tunnels should be encrypted unless OpenFlow actions explicitly + set skb mark to a non-zero value. + </p> + </column> + <column name="other_config" key="private_key" + type='{"type": "string"}'> + <p> + Name of a PEM file containing the private key used as the switch's + identity for IPsec tunnels. + </p> + </column> + <column name="other_config" key="certificate" + type='{"type": "string"}'> + <p> + Name of a PEM file containing a certificate that certifies the + switch's private key, and identifies a trustworthy switch for IPsec + tunnels. The certificate must be x.509 version 3 and with the + string in common name (CN) also set in the subject alternative name + (SAN). + </p> + </column> + <column name="other_config" key="ca_cert" + type='{"type": "string"}'> + <p> + Name of a PEM file containing the CA certificate used to verify + that a remote switch of the IPsec tunnel is trustworthy. + </p> + </column> + </group> + <group title="Common Columns"> The overall purpose of these columns is described under <code>Common Columns</code> at the beginning of this document. @@ -1379,7 +1458,7 @@ <column name="external_ids"/> </group> </table> - + <table name="Port" table="Port or bond configuration."> <p>A port within a <ref table="Bridge"/>.</p> <p>Most commonly, a port has exactly one ``interface,'' pointed to by its @@ -2368,9 +2447,9 @@ <column name="options" key="local_ip"> <p> - Optional. The tunnel destination IP that received packets must - match. Default is to match all addresses. If specified, may be one - of: + Optional (except for IPsec tunnels). The tunnel destination IP that + received packets must match. Default is to match all addresses. If + specified, may be one of: </p> <ul> @@ -2625,7 +2704,7 @@ <column name="options" key="csum" type='{"type": "boolean"}'> <p> - Optional. Compute encapsulation header (either GRE or UDP) + Optional. Compute encapsulation header (either GRE or UDP) checksums on outgoing packets. Default is disabled, set to <code>true</code> to enable. Checksums present on incoming packets will be validated regardless of this setting. @@ -2643,8 +2722,35 @@ </column> </group> - </group> + <group title="Tunnel Options: IPsec"> + <p> + <code>gre</code>, <code>geneve</code>, <code>vxlan</code>, and + <code>stt</code>interfaces support these options. + </p> + <column name="options" key="psk" type='{"type": "string"}'> + <p> + The preshared secret to negotiate tunnel in PSK mode. This value + must match on both tunnel ends and must be unset when + <code>remote_cert</code> or <code>remote_name</code>is set. + </p> + </column> + <column name="options" key="remote_cert" type='{"type": "string"}'> + <p> + Name of a PEM file containing a certificate of the remote switch. + The certificate must be x.509 version 3 and with the string in + common name (CN) also set in the subject alternative name (SAN). It + must be unset when <code>psk</code> is set. + </p> + </column> + <column name="options" key="remote_name" type='{"type": "string"}'> + <p> + Common name (CN) of the remote certificate. It must be unset when + <code>psk</code> is set. + </p> + </column> + </group> + </group> <group title="Tunnel Options: erspan only"> <p> Only <code>erspan</code> interfaces support these options. -- 2.18.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
