From patchwork Wed Jun 27 17:58:42 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Qiuyu Xiao X-Patchwork-Id: 935654 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=openvswitch.org (client-ip=140.211.169.12; helo=mail.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=) Authentication-Results: ozlabs.org; dmarc=fail (p=none dis=none) header.from=gmail.com Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="riGPRMBT"; dkim-atps=neutral Received: from mail.linuxfoundation.org (mail.linuxfoundation.org [140.211.169.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 41G9g341VMz9s2L for ; Thu, 28 Jun 2018 04:00:15 +1000 (AEST) Received: from mail.linux-foundation.org (localhost [127.0.0.1]) by mail.linuxfoundation.org (Postfix) with ESMTP id 5ECA8D4D; Wed, 27 Jun 2018 17:59:31 +0000 (UTC) X-Original-To: ovs-dev@openvswitch.org Delivered-To: ovs-dev@mail.linuxfoundation.org Received: from smtp1.linuxfoundation.org (smtp1.linux-foundation.org [172.17.192.35]) by mail.linuxfoundation.org (Postfix) with ESMTPS id A2119D30 for ; Wed, 27 Jun 2018 17:59:30 +0000 (UTC) X-Greylist: whitelisted by SQLgrey-1.7.6 Received: from mail-pl0-f42.google.com (mail-pl0-f42.google.com [209.85.160.42]) by smtp1.linuxfoundation.org (Postfix) with ESMTPS id BD441619 for ; Wed, 27 Jun 2018 17:59:25 +0000 (UTC) Received: by mail-pl0-f42.google.com with SMTP id a7-v6so1412162plp.3 for ; Wed, 27 Jun 2018 10:59:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id:in-reply-to:references; bh=oFs+4WadSCiwxkxApKT/g8HyW+24U52XYUgdqUGtY9Y=; b=riGPRMBT1u3vbI5H+y/iRy9wLskFUEJQVmg5pp0mDDX1o/NX1LPV83iPnBbRaVTLdj 7e6JO4/teG+iE0boZGjunDh4kbUlO2z3kZOTS66CGp3J/8UaFOSn6pEp3kec6b9SDyZ9 AmE4Cn2fOsVDAka5y2yjaWQN5VLgkUFMOlwGkhWf6ut71dRTsT1u1RUZRvKSlAfCZ1wj 9+Q0Aj9BM3OKV9OUuLFhFpYor8RuqjTrcMLL1iM4Rxxhi3jFm3RZyLCN8gEwZzYZlF8p G8sgead4XHk8iY/LzDhjce25gpL5wWd6VkiiMZkTgQmS6zwvIiaCcJe8Zmi47QytoZIz ncqg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references; bh=oFs+4WadSCiwxkxApKT/g8HyW+24U52XYUgdqUGtY9Y=; b=oeZsLcU1iHkm5AldrLgalpBqLKD+40LdVEFHfNmKDAZmRfx2N3zXzliba2MmDhuZal PfWQZg6fCgtGzSwVrdFSBsGMDJsauGXsDns5273ke3keqthG3g2dR6KEJTiqmPPoOkZW uKsZwYxV/O8+bZGG74G8vL+LN/2mQ6AC0nyT5gL6apGIHiA4+UDi0bnlBC2ZirFbgLsL O9muCxGW3d8/SU7k13m64DQUMQlBN/UTgPj8Iat64Kwj5mG2C6YEeo18OX01JC7TvI13 eNU18odt92wfW2umw9BgB2975WrgW5h0sssZO1iDmhku81S3HBn46LcsCuxa+GcD+qhD 9trQ== X-Gm-Message-State: APt69E3OL8OB/+TAJ5DxnUAQLIqyoefEVf1ErQxQCN8QiK6Fjc/h/azQ WYYGr86esziGVuPh4IKjuFGi1jAc X-Google-Smtp-Source: ADUXVKJPN2qGFj1wa+hhbZA6F1zZS+UPVhtXnZM388wJFOaI0p6M9dJ3JFoETOmi/2Auy1ihia+EGw== X-Received: by 2002:a17:902:b68c:: with SMTP id c12-v6mr7239804pls.114.1530122363987; Wed, 27 Jun 2018 10:59:23 -0700 (PDT) Received: from vm2.eng.vmware.com ([66.170.99.2]) by smtp.gmail.com with ESMTPSA id b62-v6sm7698187pfj.123.2018.06.27.10.59.22 (version=TLS1_2 cipher=ECDHE-RSA-AES128-SHA bits=128/128); Wed, 27 Jun 2018 10:59:23 -0700 (PDT) From: Qiuyu Xiao To: ovs-dev@openvswitch.org Date: Wed, 27 Jun 2018 10:58:42 -0700 Message-Id: <20180627175844.2809-2-qiuyu.xiao.qyx@gmail.com> X-Mailer: git-send-email 2.17.1 In-Reply-To: <20180627175844.2809-1-qiuyu.xiao.qyx@gmail.com> References: <20180627175844.2809-1-qiuyu.xiao.qyx@gmail.com> X-Spam-Status: No, score=-2.0 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, FREEMAIL_FROM, RCVD_IN_DNSWL_NONE autolearn=ham version=3.3.1 X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on smtp1.linux-foundation.org Cc: aatteka@ovn.org Subject: [ovs-dev] [PATCH 1/3] ipsec: reintroduce IPsec support for tunneling X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.12 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Sender: ovs-dev-bounces@openvswitch.org Errors-To: ovs-dev-bounces@openvswitch.org From: Ansis Atteka This patch reintroduces ovs-monitor-ipsec daemon that was previously removed by commit 2b02d770 ("openvswitch: Allow external IPsec tunnel management.") The reason for removal at the time was that there were IPsec flavoured tunnel types, like ipsec_gre, that consumed for themselves the least significant bit of SKB mark irrelevant whether Open vSwitch users were using IPsec or not. 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. Signed-off-by: Ansis Atteka --- Documentation/automake.mk | 1 + Documentation/howto/index.rst | 1 + Documentation/howto/ipsec.rst | 199 ++++++ 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 | 9 + ipsec/ovs-monitor-ipsec | 727 ++++++++++++++++++++ tests/automake.mk | 1 + tests/ovs-monitor-ipsec.at | 1076 ++++++++++++++++++++++++++++++ tests/testsuite.at | 1 + vswitchd/vswitch.xml | 83 ++- 15 files changed, 2310 insertions(+), 4 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 create mode 100644 tests/ovs-monitor-ipsec.at 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..4e4f4d211 --- /dev/null +++ b/Documentation/howto/ipsec.rst @@ -0,0 +1,199 @@ +.. + 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. + +============================================== +How to Encrypt Open vSwitch Tunnels with IPsec +============================================== + +This document describes how to use Open vSwitch integration with strongSwan +5.1 or later to provide IPsec security for STT, GENEVE, GRE and VXLAN tunnels. +This document assumes that you have already installed Open vSwitch. + + +Limitations +----------- + +There are several limitations: + +1) Currently only Debian-based platforms are supported. + +2) There is no backwards compatibility with the old IPsec implementation + that uses Racoon instead of strongSwan for IKE keying. + +3) 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. + +4) 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. + +5) 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. + + +Setup +----- + +Install strongSwan and openvswitch-ipsec debian packages:: + + $ apt-get install strongswan + $ dpkg -i openvswitch-ipsec__amd64.deb + + +Configuration +------------- + +IPsec with ovs-monitor-ipsec daemon can be configured in three +different forwarding policy 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:default_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 strongSwan 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 packets were 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:default_ipsec_skb_mark=0/1 + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) + + With this solution ovs-monitor-ipsec tells strongSwan + 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 + Remote IP: 192.168.2.129 + Tunnel Type: gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His 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 + + +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 `security guidelines +`__. + +If bug does not have security implications, then report it according to +instructions in ``__. + +There is also a possibility that there is a bug in strongSwan. In that +case report it to strongSwan mailing list. \ No newline at end of file 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 +# +# 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..12baaed55 --- /dev/null +++ b/ipsec/automake.mk @@ -0,0 +1,9 @@ +# 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 diff --git a/ipsec/ovs-monitor-ipsec b/ipsec/ovs-monitor-ipsec new file mode 100755 index 000000000..322a7045a --- /dev/null +++ b/ipsec/ovs-monitor-ipsec @@ -0,0 +1,727 @@ +#!/usr/bin/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 +from string import Template + +from ovs.db import error +from ovs.db import types +import ovs.daemon +import ovs.db.idl +import ovs.dirs +import ovs.unixctl +import ovs.unixctl.server +import ovs.util +import ovs.vlog + + +FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n" + +vlog = ovs.vlog.Vlog("ovs-monitor-ipsec") +exiting = False +keyer = None +xfrm = None + + +def unixctl_xfrm_policies(conn, unused_argv, unused_aux): + global xfrm + policies = xfrm.get_policies() + conn.reply(str(policies)) + +def unixctl_xfrm_state(conn, unused_argv, unused_aux): + global xfrm + securities = xfrm.get_securities() + conn.reply(str(securities)) + +def unixctl_ipsec_status(conn, unused_argv, unused_aux): + global keyer + conns = keyer._get_strongswan_conns() + conn.reply(str(conns)) + +def unixctl_show(conn, unused_argv, unused_aux): + global keyer + global xfrm + policies = xfrm.get_policies() + securities = xfrm.get_securities() + keyer.show(conn, policies, securities) + +def unixctl_exit(conn, unused_argv, unused_aux): + global exiting + exiting = True + conn.reply(None) + + +class XFRM(object): + """This class is a simple wrapper around ip-xfrm (8) command line + utility. 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 is destination IPv4 address and 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 is destination IPv4 address and + is SELECTOR.""" + securities = {} + proc = subprocess.Popen([self.IP, 'xfrm', 'state'], + stdout=subprocess.PIPE) + while True: + line = proc.stdout.readline().strip() + if line == '': + break + a = line.split(" ") + if len(a) >= 4 and a[0] == "sel" and a[1] == "src" and a[3] == "dst": + remote_ip = a[4].rstrip().split("/")[0] + local_ip = a[2].rstrip().split("/")[0] + if not remote_ip in securities: + securities[remote_ip] = [] + securities[remote_ip].append(line) + if not local_ip in securities: + securities[local_ip] = [] + securities[local_ip].append(line) + return securities + + +class StrongSwanTunnel(object): + """This class represents IPsec tunnel in strongSwan.""" + + transp_tmpl = {"gre" : Template("""\ +conn $ifname-$version +$auth_section + leftsubnet=%dynamic[gre] + rightsubnet=%dynamic[gre] + +"""), "gre64" : Template("""\ +conn $ifname-$version +$auth_section + leftsubnet=%dynamic[gre] + rightsubnet=%dynamic[gre] + +"""), "geneve" : Template("""\ +conn $ifname-in-$version +$auth_section + rightsubnet=%dynamic[udp/%any] + leftsubnet=%dynamic[udp/6081] + +conn $ifname-out-$version +$auth_section + rightsubnet=%dynamic[udp/6081] + leftsubnet=%dynamic[udp/%any] + +"""), "stt" : Template("""\ +conn $ifname-in-$version +$auth_section + rightsubnet=%dynamic[tcp/%any] + leftsubnet=%dynamic[tcp/7471] + +conn $ifname-out-$version +$auth_section + rightsubnet=%dynamic[tcp/7471] + leftsubnet=%dynamic[tcp/%any] + +"""), "vxlan" : Template("""\ +conn $ifname-in-$version +$auth_section + rightsubnet=%dynamic[udp/%any] + leftsubnet=%dynamic[udp/4789] + +conn $ifname-out-$version +$auth_section + rightsubnet=%dynamic[udp/4789] + leftsubnet=%dynamic[udp/%any] + +""")} + + auth_tmpl = {"psk" : Template("""\ + left=$local_ip + right=$remote_ip + authby=psk"""), + "rsa" : Template("""\ + left=$local_ip + right=$remote_ip + rightcert=ovs-$remote_ip.pem + leftcert=$certificate""")} + + unixctl_config_tmpl = Template("""\ + Remote IP: $remote_ip + Tunnel Type: $tunnel_type + Local IP: $local_ip + SKB mark: $skb_mark + Use SSL cert: $use_ssl_cert + My cert: $certificate + My key: $private_key + His cert: $peer_cert + PSK: $psk +""") + + unixctl_status_tmpl = Template("""\ + Ofport: $ofport + CFM state: $cfm_state +""") + + def __init__(self, name, row): + self.name = name # 'name' 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.cert_file = None + 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 + use_ssl_cert = options.get("use_ssl_cert") == "true" + cert = options.get("certificate") + key = options.get("private_key") + if use_ssl_cert: # Override with SSL certs if told so + (cert, key) = (keyer.public_cert, keyer.private_key) + + new_conf = { + "ifname" : self.name, + "tunnel_type" : row.type, + "remote_ip" : options.get("remote_ip"), + "local_ip" : options.get("local_ip", "0.0.0.0"), + "skb_mark" : keyer.wanted_skb_mark, + "certificate" : cert, + "private_key" : key, + "use_ssl_cert" : use_ssl_cert, + "peer_cert" : options.get("peer_cert"), + "psk" : options.get("psk")} + if self.conf != new_conf: + # Configuration was updated in OVSDB. Validate it and figure + # out what to do next with this IPsec tunnel. Also, increment + # version number 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._validate_conf(): + self.state = "CONFIGURED" + else: + vlog.warn("%s contains invalid configuration%s" % + (self.name, self.invalid_reason)) + self.state = "INVALID" + + new_status = { + "cfm_state" : "Up" if row.cfm_fault == [False] else + "Down" if row.cfm_fault == [True] else + "Disabled", + "ofport" : "Not assigned" if (row.ofport in [[], [-1]]) else + row.ofport[0]} + if self.status != new_status: + # Tunnel has become unhealthy or ofport changed. Simply log this. + vlog.dbg("%s changed status from %s to %s" % + (self.name, str(self.status), str(new_status))) + self.status = new_status + return ret + + def mark_for_removal(self): + """This function marks tunnel for removal. We can't delete tunnel + right away because we have to clean any files created for this tunnel + from it's run() function.""" + self.version += 1 + self.state = "REMOVED" + + def run(self): + """This function ensures that Tunnel's Finite State Machine moves + forward.""" + if self.last_refreshed_version == self.version: + # Configuration hasn't changed. Exit early. + return False + + if self.state == "CONFIGURED": + # If configuration changed then cleanup the old state + # and push the new state + self._cleanup_old_state() + self._push_new_state() + elif self.state == "INVALID": + # If configuration is invalid then we prevent this + # tunnel from pushing any configuration to the system + self._cleanup_old_state() + elif self.state == "REMOVED": + self._cleanup_old_state() + else: + vlog.fatal("%s is in unexpected state" % self.name) + + self.last_refreshed_version = self.version + return True + + def write_config(self, secrets, conf): + if self.conf["psk"]: + secrets.write("%s : PSK %s\n" % (self.conf["remote_ip"], + self.conf["psk"])) + auth_section = self.auth_tmpl["psk"].substitute(self.conf) + else: + secrets.write("%s : RSA %s\n" % (self.conf["remote_ip"], + self.conf["private_key"])) + auth_section = self.auth_tmpl["rsa"].substitute(self.conf) + vals = self.conf.copy() + vals["auth_section"] = auth_section + vals["version"] = self.version + conf.write(self.transp_tmpl[self.conf["tunnel_type"]].substitute(vals)) + + def show(self, policies, securities, conns): + state = self.state + if self.state == "INVALID": + state += self.invalid_reason + header = "Interface name: %s v%u (%s)\n" % (self.name, self.version, + state) + conf = self.unixctl_config_tmpl.substitute(self.conf) + status = self.unixctl_status_tmpl.substitute(self.status) + spds = "Kernel policies installed:\n" + remote_ip = self.conf["remote_ip"] + if remote_ip in policies: + for line in policies[remote_ip]: + spds += " " + line + "\n" + sas = "Kernel security associations installed:\n" + if remote_ip in securities: + for line in securities[remote_ip]: + sas += " " + line + "\n" + cons = "Strongswan connections that are active:\n" + if self.name in conns: + for tname in conns[self.name]: + cons += " " + conns[self.name][tname] + "\n" + + return header + conf + status + spds + sas + cons + "\n" + + def _validate_conf(self): + """This function verifies if IPsec tunnel has valid configuration + set in 'conf'. If it is valid, then it returns True. Otherwise, + it returns False and sets the reason why configuration was considered + as invalid. + + This function could be improved in future to also verify validness + of certificates themselves so that ovs-monitor-ipsec would not + pass malformed configuration to strongSwan.""" + + self.invalid_reason = None + if not self.conf["remote_ip"]: + self.invalid_reason = ": 'remote_ip' is not set" + elif self.conf["peer_cert"]: + if self.conf["psk"]: + self.invalid_reason = ": 'psk' must be unset with PKI" + elif not self.conf["certificate"]: + self.invalid_reason = ": must set 'certificate' with PKI" + elif not self.conf["private_key"]: + self.invalid_reason = ": must set 'private_key' with PKI" + elif self.conf["psk"]: + if self.conf["certificate"] or self.conf["private_key"]: + self.invalid_reason = ": 'certificate', 'private_key' and "\ + "'use_ssl_cert' must be unset with PSK" + if self.invalid_reason: + return False + return True + + def _cleanup_old_state(self): + if self.cert_file: + try: + os.remove(self.cert_file) + except OSError: + vlog.warn("could not remove '%s'" % (self.cert_file)) + self.cert_file = None + + def _push_new_state(self): + if self.conf["peer_cert"]: + fn = keyer.CERT_DIR + "/ovs-%s.pem" % (self.name) + try: + cert = open(fn, "w") + try: + cert.write(self.conf["peer_cert"]) + finally: + cert.close() + vlog.info("created peer certificate %s" % (fn)) + self.cert_file = fn + except (OSError, IOError) as e: + vlog.err("could not create certificate '%s'" % (fn)) + + +class StrongSwanKeyer(object): + """This class is a wrapper around strongSwan.""" + + 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=no + +conn %%default + keyingtries=%%forever + type=transport + keyexchange=ikev2 + auto=route + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +""" % (FILE_HEADER) + SHUNT_POLICY = """conn prevent_unencrypted_gre + type=drop + leftprotoport=gre + mark=%s + +conn prevent_unencrypted_stt + type=drop + leftprotoport=tcp/7471 + mark=%s + +""" + + def __init__(self, strongswan_root_prefix): + self.private_key = None + self.public_cert = None + self.wanted_skb_mark = None + self.in_use_skb_mark = None + self.tunnels = {} + self.CERT_DIR = strongswan_root_prefix + "/etc/ipsec.d/certs" + self.CHARON_CONF = strongswan_root_prefix + "/etc/strongswan.d/ovs.conf" + self.IPSEC = strongswan_root_prefix + "/usr/sbin/ipsec" + self.IPSEC_CONF = strongswan_root_prefix + "/etc/ipsec.conf" + self.IPSEC_SECRETS = strongswan_root_prefix + "/etc/ipsec.secrets" + + def 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 StrongSwanTunnel.transp_tmpl + + def is_ipsec_required(self, options_column): + """Return True if tunnel needs to be encrypted. Otherwise, + returns False.""" + return "psk" in options_column or "peer_cert" in options_column + + def restart(self): + """This function restarts strongSwan.""" + self._initial_configuration() + vlog.info("restarting strongSwan") + subprocess.call([self.IPSEC, "restart"]) + + def add_tunnel(self, name, row): + """Adds a new tunnel that keyer will provision with 'name'.""" + vlog.info("Tunnel %s appeared in OVSDB" % (name)) + self.tunnels[name] = StrongSwanTunnel(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_ssl_credentials(self, credentials): + """Updates already existing """ + self.public_cert = credentials[0] + self.private_key = credentials[1] + + def update_skb_mark(self, skb_mark): + """Update skb_mark used in IPsec policies.""" + self.wanted_skb_mark = skb_mark + + def show(self, unix_conn, policies, securities): + """This function prints all tunnel state in 'unix_conn'. + It uses 'policies' and securities' received from Linux Kernel + to show if tunnels were actually configured by strongSwan.""" + if not self.tunnels: + unix_conn.reply("No tunnels configured with IPsec") + return + s = "" + conns = self._get_strongswan_conns() + for name, tunnel in self.tunnels.iteritems(): + s += tunnel.show(policies, securities, conns) + unix_conn.reply(s) + + def run(self): + """This function runs state machine that represents whole + strongSwan configuration (i.e. merged together from individual + tunnel state machines). It creates configuration files and + tells strongSwan to update configuration.""" + needs_refresh = False + removed_tunnels = [] + ipsec_secrets = open(self.IPSEC_SECRETS, "w") + ipsec_conf = open(self.IPSEC_CONF, "w") + ipsec_secrets.write(FILE_HEADER) + ipsec_conf.write(self.CONF_HEADER) + if self.in_use_skb_mark != self.wanted_skb_mark: + self.in_use_skb_mark = self.wanted_skb_mark + needs_refresh = True + + if self.in_use_skb_mark: + ipsec_conf.write(" mark_out=%s\n" % self.in_use_skb_mark) + ipsec_conf.write(self.SHUNT_POLICY % (self.in_use_skb_mark, self.in_use_skb_mark)) + + for name, tunnel in self.tunnels.iteritems(): + if tunnel.run(): + needs_refresh = True + + if tunnel.state == "REMOVED": + removed_tunnels.append(name) + elif tunnel.state == "CONFIGURED": + tunnel.write_config(ipsec_secrets, ipsec_conf) + ipsec_secrets.close() + ipsec_conf.close() + + for name in removed_tunnels: + del self.tunnels[name] + + if needs_refresh: + self._refresh() + + def _refresh(self): + """This functions refreshes strongSwan configuration. Behind the + scenes this function calls: + 1. once "ipsec update" command that tells strongSwan to load + all new tunnels from "ipsec.conf"; and + 2. once "ipsec rereadsecrets" command that tells strongswan to load + secrets from "ipsec.conf" file + 3. for every removed tunnel "ipsec stroke down-nb " 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 " + # command. We use 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_strongswan_conns() + for ifname, conns in conns_dict.iteritems(): + tunnel = self.tunnels.get(ifname) + for conn in conns: + # IPsec "connection" names that we choose in strongswan + # must start with Interface name + if not conn.startswith(ifname): + vlog.err("%s does not start with %s" % (conn, ifname)) + continue + + # version number should be the first integer after + # interface name in IPsec "connection" + try: + ver = int(re.findall(r'\d+', conn[len(ifname):])[0]) + except ValueError, IndexError: + vlog.err("%s does not contain version number") + continue + + if not tunnel or tunnel.version != ver: + vlog.info("%s is outdated %u" % (conn, ver)) + subprocess.call([self.IPSEC, "stroke", "down-nb", conn]) + + + def _get_strongswan_conns(self): + """This function parses output from 'ipsec status' command. + It returns dictionary where is interface name (as in OVSDB) + and is another dictionary. This another dictionary + uses strongSwan connection name as and more detailed + sample line from the parsed outpus as . """ + + conns = {} + proc = subprocess.Popen([self.IPSEC, 'status'], + stdout=subprocess.PIPE) + while True: + line = proc.stdout.readline().strip() + if line == '': + break + tunnel_name = line.split(":") + if len(tunnel_name) < 2: + continue + ifname = tunnel_name[0].split("-") + if len(ifname) < 2: + continue + + if not ifname[0] in conns: + conns[ifname[0]] = {} + (conns[ifname[0]])[tunnel_name[0]] = line + + return conns + + def _initial_configuration(self): + """This function creates initial configuration that strongSwan should + receive on startup.""" + f = open(self.CHARON_CONF, "w") + f.write(self.STRONGSWAN_CONF) + f.close() + + f = open(self.IPSEC_CONF, "w") + f.write(self.CONF_HEADER) + f.close() + + f = open(self.IPSEC_SECRETS, "w") + f.write(FILE_HEADER) + f.close() + + +def read_ovsdb_open_vswitch_table(data): + """This functions reads IPsec relevant configuration from Open_vSwitch + table.""" + ssl = (None, None) + global_ipsec_skb_mark = None + for row in data["Open_vSwitch"].rows.itervalues(): + if row.ssl: + ssl = (row.ssl[0].certificate, row.ssl[0].private_key) + global_ipsec_skb_mark = row.other_config.get("default_ipsec_skb_mark") + keyer.update_ssl_credentials(ssl) + keyer.update_skb_mark(global_ipsec_skb_mark) + +def read_ovsdb_interface_table(data): + """This function reads the IPsec relevant configuration from Interface + table.""" + ifaces = set() + for row in data["Interface"].rows.itervalues(): + if not keyer.is_tunneling_type_supported(row.type): + continue + if not keyer.is_ipsec_required(row.options): + continue + if row.name in keyer.tunnels: + keyer.update_tunnel(row.name, row) + else: + keyer.add_tunnel(row.name, row) + ifaces.add(row.name) + # Mark for removal those tunnels that just disappeared from OVSDB + for tunnel in keyer.tunnels.keys(): + if not tunnel in ifaces: + keyer.del_tunnel(tunnel) + +def read_ovsdb(data): + """This function reads all configuration from OVSDB that ovs-monitor-ipsec + is interested in.""" + read_ovsdb_open_vswitch_table(data) + read_ovsdb_interface_table(data) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("database", metavar="DATABASE", + help="A socket on which ovsdb-server is listening.") + parser.add_argument("--root-prefix", metavar="DIR", + help="Use DIR as alternate root directory" + " (for testing).") + + ovs.vlog.add_args(parser) + ovs.daemon.add_args(parser) + args = parser.parse_args() + ovs.vlog.handle_args(args) + ovs.daemon.handle_args(args) + + global keyer + global xfrm + + root_prefix = args.root_prefix if args.root_prefix else "" + xfrm = XFRM(root_prefix) + keyer = StrongSwanKeyer(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", ["ssl", "other_config"]) + schema_helper.register_columns("SSL", ["certificate", "private_key"]) + idl = ovs.db.idl.Idl(remote, schema_helper) + + ovs.daemon.daemonize() + + ovs.unixctl.command_register("xfrm/policies", "", 0, 0, + unixctl_xfrm_policies, None) + ovs.unixctl.command_register("xfrm/state", "", 0, 0, + unixctl_xfrm_state, None) + ovs.unixctl.command_register("ipsec/status", "", 0, 0, + unixctl_ipsec_status, None) + ovs.unixctl.command_register("tunnels/show", "", 0, 0, + unixctl_show, None) + ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None) + + error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None) + if error: + ovs.util.ovs_fatal(error, "could not create unixctl server", vlog) + + seqno = idl.change_seqno # Sequence number when OVSDB was processed last time + keyer.restart() + while True: + unixctl_server.run() + if exiting: + break + + idl.run() + if seqno != idl.change_seqno: + read_ovsdb(idl.tables) + seqno = idl.change_seqno + + keyer.run() + + poller = ovs.poller.Poller() + unixctl_server.wait(poller) + idl.wait(poller) + poller.block() + + unixctl_server.close() + idl.close() + + +if __name__ == '__main__': + try: + main() + except SystemExit: + # Let system.exit() calls complete normally + raise + except: + vlog.exception("traceback") + sys.exit(ovs.daemon.RESTART_EXIT_CODE) diff --git a/tests/automake.mk b/tests/automake.mk index 8224e5a4a..703b4fa50 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -60,6 +60,7 @@ TESTSUITE_AT = \ tests/lockfile.at \ tests/reconnect.at \ tests/ovs-vswitchd.at \ + tests/ovs-monitor-ipsec.at \ tests/dpif-netdev.at \ tests/dpctl.at \ tests/ofproto-dpif.at \ diff --git a/tests/ovs-monitor-ipsec.at b/tests/ovs-monitor-ipsec.at new file mode 100644 index 000000000..ec537ad32 --- /dev/null +++ b/tests/ovs-monitor-ipsec.at @@ -0,0 +1,1076 @@ +# WAIT_FOR_TUNNEL_TO_UPDATE([tunnel], [version], [state]) +# +# This macro should be called after ovs-vsctl command that updated +# IPsec configuration in OVSDB. It basically serves as synchronization +# barrier to ensures that ovs-monitor-ipsec daemon's internal configuration +# for 'tunnel' has updated to 'version' and is in 'state'. +m4_define([WAIT_FOR_TUNNEL_TO_UPDATE], [ +OVS_WAIT_UNTIL([ovs-appctl -t ovs-monitor-ipsec tunnels/show | grep "Interface name: $1 v$2 ($3"]) +]) + + +# WAIT_FOR_PROPER_IPSEC_CLEANUP([]) +# +# This macro should be called *only* after all IPsec tunnels were removed +# from OVSDB Interface table. This macro verifies that: +# 1) ovs-monitor-ipsec daemon thinks that there are no IPsec tunnels +# 2) ipsec.conf file does not contain any tunnels +# 3) ipsec.secrets does not contain any 'secrets' for any tunnels +# 4) ipsec.d directory does not contain any certificates that were +# previously created by ovs-monitor-ipsec daemon +m4_define([WAIT_FOR_PROPER_IPSEC_CLEANUP], [ +OVS_WAIT_UNTIL([ovs-appctl -t ovs-monitor-ipsec tunnels/show | grep "No tunnels configured with IPsec"]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +]) +AT_CHECK([ls etc/ipsec.d/certs], [0], [dnl]) +]) + + +# VERIFY_IPSEC_COMMANDS_EXECUTED([ipsec_commands]) +# +# This macro alows to verify that ovs-monitor-ipsec daemon called correct +# 'ipsec' commands. All ipsec commands are logged by a script that mocks +# 'ipsec' utility. +# This macro filters out "ipsec rereadsecrets" and "ipsec status" +# commands. +m4_define([VERIFY_IPSEC_COMMANDS_EXECUTED], [ +AT_CHECK([egrep -v "ipsec status|ipsec rereadsecrets" actions], [0], [$1]) +]) + + +# INIT_SSL_CERTS([]) +# +# This macro creates sample SSL certificates so that it would be +# possible to test tunnels that use "use_ssl_certs" setting. Don't +# use these certificates in production. +m4_define([INIT_SSL_CERTS], [ +AT_DATA([ssl_key.pem], [ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxd9ZkUK+sBToEb8x1LSoMfVPw+l4ww915BNn/0TSA7yY+lq/ +k14+FYHFdqB9vSh5C0bUdhrE7QJ451WP8NyTDIUuZlieUBzonXy0hYVBQ8bUSGfF +go/4IWb/tHKIlTiZ86GqaBc8uddiZ7cF2beZwv9un9ZHOGtQrvMdzB7FzmzZraJ1 +NbAsr1GX4w0euBjPbUyu96nzBeKWqX3vdERK1ddL5y8hA2kSUslci60zCf9/jvcV +aSDvcpLwxKq2QYeS8caPWLQmyszyBrdIvsnrTcUPIG+OOs/w3M+KNYKrySF12hKU +bUujIrBPt+RJ9ptkv28TYRYqnigRZF3nzNnjewIDAQABAoIBAQCqWh2cZ6APrBAX +p0lZXKcpS47+lbQ4CsluMB7qr+829FmnwBUK5KoCjhTYild2UK/VO4eSnn1Hp0c7 +sngX325h6w8FYen6AslpPIGWKiEEHtuH8n8iZpwy9Z/TVH+uKGqyS06QCuFnBb1c +mT9aLy0bqhktVqc+NXXjCL9wilW13pAMh56zYQ8oMJUvNLLknttv9dYmuIyZ9hg/ +5al4pN7OqzC9N4AAgDjyHIn3N6t6yfBc/kcJ9OdJ1709DYIr6Nl500Nhf+GZkrEX +1KTeWPzV6GQD7FUZTJ5eZWNnLbD03823Q0HCElGQkboCUU65EdVdNEHvGdPZw74d +t2iaXPxxAoGBAPGvd+PByAH98KKeu9z5ysc0AgVOIDTlqhReqMc2jnYTCglWlMcx +al7K0T6QcSTl3Y2xgGo5O7P+MzZkIiKFPx5fX2d9x6CybEcOQ+5bd6fI0hYF8W18 +i+i/mmJgGo33UOW6EQfYKAHLn4WdlsKJ9aTaNNCanqkqmS1l+GsrRgYdAoGBANGX +kl/kfGVYOSRh6ztnRc0i3ONo4RZrDgV0bjEmmujc7V5CIDPxhMUkpsOcG8iCt+55 +4JqdtLbmfWfeTWWafn8j/Y9DsSv4U0MHc6+Hfit1dmYmZ+ixHjzul9m6H7XsYZ2k +IpNa7RN+MqvB66pHGpTcC8IQ2fNIkK3GtWLaA3x3AoGBAOX90xtcZxbuLzax05jf +5MZYiau+wwtTmtyzj+2zzzIxwBVO3VoJfm4il6jwD5vLW2Dhj5CGUnhg6R9TfuBW +6M/gdounuHcGE+AyhRao2F9EzhfDJBLKuOGOpD4Fsn9y4Psca+SJINlEitO+OZ97 +ZdWxCR2SZnYZYZdAOHzTu1lJAoGAJcxK9oYzNOerLneGP6lJOkx+P3jLlwppdexg +bvbCWxp0qFoOiq+UvST1+jLuA8QnPZe3PMsSKyX4GcJKfPdWtsEb2jlf+0kGYwE2 +CMLLqzS8zIFCngFLLbvtoLNjQqDFnfNa1O5B8RECPF11jbjS/2OLr0zwsWI1zVEX +pyMgG9MCgYBb+PUIVpo5vRMTsyQ5whRQPK7+5jbU+wFKMnryqfsZ/K6DfeoZzAmn +KhjeRveOiYEfQZfI5GSfm97TKX5hYKQSwOHa9yzTD25iw4aleU8hc4IbqANzbr/c +59KynTtKfZXSb/DAd/FTtFvzfwiin5ss4INKbpuBACDrb+mj1nY6bw== +-----END RSA PRIVATE KEY----- +]) +AT_DATA([ssl_cert.pem], [ +-----BEGIN CERTIFICATE----- +MIIDgjCCAmoCAQQwDQYJKoZIhvcNAQEFBQAwgYExCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTEVMBMGA1UEChMMT3BlbiB2U3dpdGNoMREwDwYDVQQLEwhzd2l0Y2hj +YTE7MDkGA1UEAxMyT1ZTIHN3aXRjaGNhIENBIENlcnRpZmljYXRlICgyMDE1IEZl +YiAwOSAwNjoyNjowMykwHhcNMTUwMzEwMTcxMTEzWhcNMjUwMzA3MTcxMTEzWjCB +izELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRUwEwYDVQQKEwxPcGVuIHZTd2l0 +Y2gxHzAdBgNVBAsTFk9wZW4gdlN3aXRjaCBjZXJ0aWZpZXIxNzA1BgNVBAMTLmNs +aWVudCBpZDplMWQxNmQ2MS1jMDcwLTQxOWQtOTYxYi00M2Q4YTBkM2IwMzAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDF31mRQr6wFOgRvzHUtKgx9U/D +6XjDD3XkE2f/RNIDvJj6Wr+TXj4VgcV2oH29KHkLRtR2GsTtAnjnVY/w3JMMhS5m +WJ5QHOidfLSFhUFDxtRIZ8WCj/ghZv+0coiVOJnzoapoFzy512JntwXZt5nC/26f +1kc4a1Cu8x3MHsXObNmtonU1sCyvUZfjDR64GM9tTK73qfMF4papfe90RErV10vn +LyEDaRJSyVyLrTMJ/3+O9xVpIO9ykvDEqrZBh5Lxxo9YtCbKzPIGt0i+yetNxQ8g +b446z/Dcz4o1gqvJIXXaEpRtS6MisE+35En2m2S/bxNhFiqeKBFkXefM2eN7AgMB +AAEwDQYJKoZIhvcNAQEFBQADggEBADmkUbvx9zVtcw1/kSMzr184J3xNBZhmJZ39 +RBXKR1elZ1aVjc4406vsKZiKKWKQA7rC51xzaLk/huKvUez8RU346GugHtUus05x +7MZ2DDiwJNb7hJBBiJ9a0ZyhwCBcQJj95g3686TWYGpc8eEPldACf38SP2etPOmE +QPvIMm3pztNf6JZDFDoV3cwzVEFhQnDHXQE9sR6eLhxpbQqwVJ8tmAr9F5hRr4i7 +SpxHclvZyS+c3nsNb0xluN1Dh8fcy2ITSXfpar+H5B/i1nZA0dBJsW7ChRHAhPQU +QYLXEK2+YnilIr00RMv7qa0vF6vC0lXHjGZhm5GzHW2hXDDUIig= +-----END CERTIFICATE----- +]) +AT_CHECK([ovs-vsctl set Open_vSwitch . ssl=@N1 -- --id=@N1 create SSL private_key=ssl_key.pem certificate=ssl_cert.pem bootstrap_ca_cert=true], [0], [ignore]) +]) + + + +AT_BANNER([ovs-monitor-ipsec (XFRM)]) + +# This test ensures that ovs-monitor-ipsec would properly parse +# IPsec policies from kernel. +AT_SETUP([Parse "ip xfrm policy" output]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_MONITOR_IPSEC_START + +AT_DATA([sbin/ip], [#!/bin/sh +echo 'src 192.168.2.128/32 dst 192.168.2.129/32 proto gre + dir in priority 1794 + mark 1/0x1 + tmpl src 0.0.0.0 dst 0.0.0.0 + proto esp reqid 1 mode transport +src 192.168.2.129/32 dst 192.168.2.128/32 proto gre + dir out priority 1794 + mark 1/0x1 + tmpl src 0.0.0.0 dst 0.0.0.0 + proto esp reqid 1 mode transport' +]) +chmod +x sbin/ip +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec xfrm/policies], [0], [dnl +{'192.168.2.129': [['src 192.168.2.128/32 dst 192.168.2.129/32 proto gre', 'src 192.168.2.128/32 dst 192.168.2.129/32 proto gre']], '192.168.2.128': [['src 192.168.2.129/32 dst 192.168.2.128/32 proto gre', 'src 192.168.2.129/32 dst 192.168.2.128/32 proto gre']]} +]) + +OVS_MONITOR_IPSEC_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly parse +# IPsec security associations from kernel. +AT_SETUP([Parse "ip xfrm state" output]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_MONITOR_IPSEC_START + +AT_DATA([sbin/ip], [#!/bin/sh +echo 'src 192.168.2.129 dst 192.168.2.128 + proto esp spi 0xcdfe5007 reqid 1 mode transport + replay-window 32 + mark 1/0x1 + aead rfc4106(gcm(aes)) 0x3d7cb9ddde30c4e6a870b9c1474b8795a85e564d 96 + sel src 192.168.2.129/32 dst 192.168.2.128/32 +src 192.168.2.128 dst 192.168.2.129 + proto esp spi 0xca623718 reqid 1 mode transport + replay-window 32 + mark 1/0x1 + aead rfc4106(gcm(aes)) 0x7c5b209afe3c93adae68f544f5a1326320b15fe5 96 + sel src 192.168.2.128/32 dst 192.168.2.129/32' +]) +chmod +x sbin/ip +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec xfrm/state], [0], [dnl +{'192.168.2.129': [['sel src 192.168.2.129/32 dst 192.168.2.128/32', 'sel src 192.168.2.128/32 dst 192.168.2.129/32']], '192.168.2.128': [['sel src 192.168.2.129/32 dst 192.168.2.128/32', 'sel src 192.168.2.128/32 dst 192.168.2.129/32']]} +]) +OVS_MONITOR_IPSEC_STOP +AT_CLEANUP + + +AT_BANNER([ovs-monitor-ipsec (strongSwan)]) + +# This test ensures that ovs-monitor-ipsec would ignore non-IPsec tunnels. +AT_SETUP([Ignore non-IPsec tunnels]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- set interface gre0 type=gre options:remote_ip=1.2.3.4]) +# Since ovs-monitor-ipsec does not keep track of non-IPsec tunnels then there +# is no proper way to synchronize this. If ovs-monitor-ipsec did not +# ignore non-IPsec tunnels then this test would occasionally fail. +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +No tunnels configured with IPsec +]) +AT_CHECK([ovs-vsctl del-port br0 gre0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly parse +# output from "ipsec status" command +AT_SETUP([Parse "ipsec status" output]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_MONITOR_IPSEC_START + +AT_DATA([active_tunns], [Routed Connections: + stt0-out-2{3}: ROUTED, TRANSPORT + stt0-out-2{3}: 192.168.2.128/32[[tcp]] === 192.168.2.129/32[[tcp/7471]] + stt0-in-2{2}: ROUTED, TRANSPORT + stt0-in-2{2}: 192.168.2.128/32[[tcp/7471]] === 192.168.2.129/32[[tcp]] +Security Associations (1 up, 0 connecting): + stt0-in-2[[3]]: ESTABLISHED 3 minutes ago, 192.168.2.128[[192.168.2.128]]...192.168.2.129[[192.168.2.129]] + stt0-in-2{2}: INSTALLED, TRANSPORT, ESP SPIs: cbc3e903_i c29b1e5c_o + stt0-in-2{2}: 192.168.2.128/32[[tcp/7471]] === 192.168.2.129/32[[tcp]] + stt0-out-2{3}: INSTALLED, TRANSPORT, ESP SPIs: c958d9fb_i c45ce6b0_o + stt0-out-2{3}: 192.168.2.128/32[[tcp]] === 192.168.2.129/32[[tcp/7471]] +]) + +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec ipsec/status], [0], [dnl +{'stt0': {'stt0-in-2[[3]]': 'stt0-in-2[[3]]: ESTABLISHED 3 minutes ago, 192.168.2.128[[192.168.2.128]]...192.168.2.129[[192.168.2.129]]:', 'stt0-in-2{2}': 'stt0-in-2{2}: 192.168.2.128/32[[tcp/7471]] === 192.168.2.129/32[[tcp]]:', 'stt0-out-2{3}': 'stt0-out-2{3}: 192.168.2.128/32[[tcp]] === 192.168.2.129/32[[tcp/7471]]:'}} +]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +OVS_MONITOR_IPSEC_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly configure +# ipsec_gre tunnel with Pre-Shared Key +AT_SETUP([ipsec_gre with PSK authentication]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- \ + set interface gre0 type=ipsec_gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) +WAIT_FOR_TUNNEL_TO_UPDATE([gre0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: gre0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + gre0-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : PSK swordfish +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn gre0-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + leftsubnet=%dynamic[[gre]] + rightsubnet=%dynamic[[gre]] + +]) +AT_CHECK([ovs-vsctl del-port gre0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb gre0-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly configure +# ipsec_stt tunnel with Pre-Shared Key +AT_SETUP([ipsec_stt with PSK authentication]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 stt0 -- \ + set interface stt0 type=ipsec_stt \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) +WAIT_FOR_TUNNEL_TO_UPDATE([stt0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: stt0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_stt + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + stt0-in-1: + stt0-out-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : PSK swordfish +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn stt0-in-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[tcp/%any]] + leftsubnet=%dynamic[[tcp/7471]] + +conn stt0-out-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[tcp/7471]] + leftsubnet=%dynamic[[tcp/%any]] + +]) +AT_CHECK([ovs-vsctl del-port stt0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb stt0-in-1 +ipsec stroke down-nb stt0-out-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly configure +# ipsec_geneve tunnel with Pre-Shared Key +AT_SETUP([ipsec_geneve with PSK authentication]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 geneve0 -- \ + set interface geneve0 type=ipsec_geneve \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) +WAIT_FOR_TUNNEL_TO_UPDATE([geneve0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: geneve0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_geneve + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + geneve0-in-1: + geneve0-out-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : PSK swordfish +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn geneve0-in-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[udp/%any]] + leftsubnet=%dynamic[[udp/6081]] + +conn geneve0-out-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[udp/6081]] + leftsubnet=%dynamic[[udp/%any]] + +]) +AT_CHECK([ovs-vsctl del-port geneve0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb geneve0-in-1 +ipsec stroke down-nb geneve0-out-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would properly configure +# ipsec_vxlan tunnel with Pre-Shared Key +AT_SETUP([ipsec_vxlan with PSK authentication]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 vxlan0 -- \ + set interface vxlan0 type=ipsec_vxlan \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) +WAIT_FOR_TUNNEL_TO_UPDATE([vxlan0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: vxlan0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_vxlan + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + vxlan0-in-1: + vxlan0-out-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : PSK swordfish +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn vxlan0-in-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[udp/%any]] + leftsubnet=%dynamic[[udp/4789]] + +conn vxlan0-out-1 + left=0.0.0.0 + right=1.2.3.4 + authby=psk + rightsubnet=%dynamic[[udp/4789]] + leftsubnet=%dynamic[[udp/%any]] + +]) +AT_CHECK([ovs-vsctl del-port vxlan0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb vxlan0-in-1 +ipsec stroke down-nb vxlan0-out-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec is able to configure IPsec for +# a tunnel that wants to reuse Open vSwitch SSL certificates for +# authentication. Note that previous PSK tests already verified geneve, +# vxlan and stt IPsec tunneling. So there is not much benefit of redoing +# these kind of tests for those tunneling protocols. +AT_SETUP([ipsec_gre with PKI authentication (use_ssl_cert)]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START +INIT_SSL_CERTS + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- \ + set interface gre0 type=ipsec_gre \ + options:remote_ip=1.2.3.4 \ + options:use_ssl_cert=true \ + options:peer_cert="asd"]) +WAIT_FOR_TUNNEL_TO_UPDATE([gre0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: gre0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: True + My cert: ssl_cert.pem + My key: ssl_key.pem + His cert: asd + PSK: None + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + gre0-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : RSA ssl_key.pem +]) +AT_CHECK([cat etc/ipsec.d/certs/ovs-gre0.pem], [0], [dnl +asd]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn gre0-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=ssl_cert.pem + leftsubnet=%dynamic[[gre]] + rightsubnet=%dynamic[[gre]] + +]) +AT_CHECK([ovs-vsctl del-port gre0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb gre0-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec would be able to configure +# IPsec tunnel that has it's own Private and Public key (opposed to +# reusing Open vSwitch's SSL credentials) +AT_SETUP([ipsec_gre with PKI authentication]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- \ + set interface gre0 type=ipsec_gre \ + options:remote_ip=1.2.3.4 \ + options:private_key="private_key.pem" \ + options:certificate="public_key.pem" \ + options:peer_cert="asd"]) +WAIT_FOR_TUNNEL_TO_UPDATE([gre0], [1], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: gre0 v1 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: public_key.pem + My key: private_key.pem + His cert: asd + PSK: None + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + gre0-1: + +]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : RSA private_key.pem +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn gre0-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=public_key.pem + leftsubnet=%dynamic[[gre]] + rightsubnet=%dynamic[[gre]] + +]) +AT_CHECK([ovs-vsctl del-port gre0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec stroke down-nb gre0-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec is able to configure +# two IPsec tunnels without interfering with each other. +AT_SETUP([ipsec_gre and ipsec_stt to two different hosts]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- \ + set interface gre0 type=ipsec_gre \ + options:remote_ip=1.2.3.4 \ + options:private_key="private_key1.pem" \ + options:certificate="public_key1.pem" \ + options:peer_cert="asd"]) +AT_CHECK([ovs-vsctl add-port br0 stt0 -- \ + set interface stt0 type=ipsec_stt \ + options:remote_ip=1.2.3.5 \ + options:private_key="private_key2.pem" \ + options:certificate="public_key2.pem" \ + options:peer_cert="fgh"]) +WAIT_FOR_TUNNEL_TO_UPDATE([gre0], [1], [CONFIGURED]) +WAIT_FOR_TUNNEL_TO_UPDATE([stt0], [1], [CONFIGURED]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : RSA private_key1.pem +1.2.3.5 : RSA private_key2.pem +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn gre0-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=public_key1.pem + leftsubnet=%dynamic[[gre]] + rightsubnet=%dynamic[[gre]] + +conn stt0-in-1 + left=0.0.0.0 + right=1.2.3.5 + rightcert=ovs-1.2.3.5.pem + leftcert=public_key2.pem + rightsubnet=%dynamic[[tcp/%any]] + leftsubnet=%dynamic[[tcp/7471]] + +conn stt0-out-1 + left=0.0.0.0 + right=1.2.3.5 + rightcert=ovs-1.2.3.5.pem + leftcert=public_key2.pem + rightsubnet=%dynamic[[tcp/7471]] + leftsubnet=%dynamic[[tcp/%any]] + +]) +AT_CHECK([ovs-vsctl del-port gre0]) +AT_CHECK([ovs-vsctl del-port stt0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec update +ipsec stroke down-nb gre0-1 +ipsec update +ipsec stroke down-nb stt0-in-1 +ipsec stroke down-nb stt0-out-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec is able to configure two +# IPsec tunnels simulatenously to the same remote_ip. +AT_SETUP([ipsec_gre and ipsec_stt to the same host]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + + +AT_CHECK([ovs-vsctl add-port br0 gre0 -- \ + set interface gre0 type=ipsec_gre \ + options:remote_ip=1.2.3.4 \ + options:private_key="private_key1.pem" \ + options:certificate="public_key1.pem" \ + options:peer_cert="asd"]) +AT_CHECK([ovs-vsctl add-port br0 stt0 -- \ + set interface stt0 type=ipsec_stt \ + options:remote_ip=1.2.3.4 \ + options:private_key="private_key2.pem" \ + options:certificate="public_key2.pem" \ + options:peer_cert="fgh"]) +WAIT_FOR_TUNNEL_TO_UPDATE([stt0], [1], [CONFIGURED]) +WAIT_FOR_TUNNEL_TO_UPDATE([stt0], [1], [CONFIGURED]) +AT_CHECK([cat etc/ipsec.secrets], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + +1.2.3.4 : RSA private_key1.pem +1.2.3.4 : RSA private_key2.pem +]) +AT_CHECK([cat etc/ipsec.conf], [0], [dnl +# Generated by ovs-monitor-ipsec...do not modify by hand! + + +config setup + uniqueids=no + +conn %default + keyingtries=%forever + type=transport + keyexchange=ikev2 + auto=route + mark_in=1/1 + ike=aes128gcm12-aesxcbc-modp1024 + esp=aes128gcm12-modp1024 + +conn gre0-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=public_key1.pem + leftsubnet=%dynamic[[gre]] + rightsubnet=%dynamic[[gre]] + +conn stt0-in-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=public_key2.pem + rightsubnet=%dynamic[[tcp/%any]] + leftsubnet=%dynamic[[tcp/7471]] + +conn stt0-out-1 + left=0.0.0.0 + right=1.2.3.4 + rightcert=ovs-1.2.3.4.pem + leftcert=public_key2.pem + rightsubnet=%dynamic[[tcp/7471]] + leftsubnet=%dynamic[[tcp/%any]] + +]) +AT_CHECK([ovs-vsctl del-port gre0]) +AT_CHECK([ovs-vsctl del-port stt0]) + +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec update +ipsec stroke down-nb gre0-1 +ipsec update +ipsec stroke down-nb stt0-in-1 +ipsec stroke down-nb stt0-out-1 +]) +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP +AT_CLEANUP + + +# This test ensures that ovs-monitor-ipsec can properly update IPsec tunnel's +# configuration by calling "ipsec update" and "ipsec stroke down-nb" +# commands whenver OVSDB configuration changes. +AT_SETUP([Sequence of IPsec tunnel reconfiguration events]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +AT_SKIP_IF([$non_ascii_cwd]) +OVS_VSWITCHD_START([]) +OVS_MONITOR_IPSEC_START + +# Start with tunnel that has 'type' set to 'ipsec_stt' and nothing else +ovs-vsctl add-port br0 tun0 -- set interface tun0 type=ipsec_stt +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [1], [INVALID]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v1 (INVALID: 'remote_ip' is not set) + Remote IP: None + Tunnel Type: ipsec_stt + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: None + Ofport: Not assigned + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + +]) + +ovs-vsctl set interface tun0 options:remote_ip="1.2.3.4" +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [2], [INVALID]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v2 (INVALID: must set either 'psk' or 'peer_cert') + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_stt + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: None + Ofport: Not assigned + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + +]) + +ovs-vsctl set interface tun0 options:psk=swordfish +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [3], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v3 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_stt + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + tun0-out-3: + tun0-in-3: + +]) + + +ovs-vsctl set interface tun0 type=ipsec_gre +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [4], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v4 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: None + PSK: swordfish + Ofport: 1 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + tun0-4: + +]) + + +ovs-vsctl remove interface tun0 options psk --\ + set Interface tun0 options:peer_cert="asd" +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [5], [INVALID]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v5 (INVALID: must set 'certificate' with PKI) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: None + My key: None + His cert: asd + PSK: None + Ofport: Not assigned + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + +]) + + +ovs-vsctl set Interface tun0 options:certificate="my_cert.pem" +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [6], [INVALID]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v6 (INVALID: must set 'private_key' with PKI) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: my_cert.pem + My key: None + His cert: asd + PSK: None + Ofport: 2 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + +]) + + +ovs-vsctl set Interface tun0 options:private_key="my_key.pem" +WAIT_FOR_TUNNEL_TO_UPDATE([tun0], [7], [CONFIGURED]) +AT_CHECK([ovs-appctl -t ovs-monitor-ipsec tunnels/show], [0], [dnl +Interface name: tun0 v7 (CONFIGURED) + Remote IP: 1.2.3.4 + Tunnel Type: ipsec_gre + Local IP: 0.0.0.0 + Use SSL cert: False + My cert: my_cert.pem + My key: my_key.pem + His cert: asd + PSK: None + Ofport: 2 + CFM state: Disabled +Kernel policies installed: +Kernel security associations installed: +Strongswan connections that are active: + tun0-7: + +]) + +AT_CHECK([ovs-vsctl del-port tun0]) +WAIT_FOR_PROPER_IPSEC_CLEANUP +VERIFY_IPSEC_COMMANDS_EXECUTED([dnl +ipsec restart +ipsec update +ipsec update +ipsec update +ipsec update +ipsec stroke down-nb tun0-in-3 +ipsec stroke down-nb tun0-out-3 +ipsec update +ipsec stroke down-nb tun0-4 +ipsec update +ipsec update +ipsec update +ipsec stroke down-nb tun0-7 +]) +# For this test all errors in OVS logs that contain 'netdev' must be ignored +# because ovs-vswitchd also does basic validation +OVS_MONITOR_IPSEC_STOP +OVS_VSWITCHD_STOP(["/netdev/d"]) +AT_CLEANUP diff --git a/tests/testsuite.at b/tests/testsuite.at index 15c385e2c..23b7defbd 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -55,6 +55,7 @@ m4_include([tests/ovs-router.at]) m4_include([tests/lockfile.at]) m4_include([tests/reconnect.at]) m4_include([tests/ovs-vswitchd.at]) +m4_include([tests/ovs-monitor-ipsec.at]) m4_include([tests/ofproto.at]) m4_include([tests/dpif-netdev.at]) m4_include([tests/pmd.at]) diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml index b93da69bd..205578c87 100644 --- a/vswitchd/vswitch.xml +++ b/vswitchd/vswitch.xml @@ -473,8 +473,15 @@ OpenFlow specification mandates the timeout to be at least one second. The default is 10 seconds.

- - + + +

+ Defines the value and mask of SKB mark to use in IPsec shunt + policies. There are three packet forwarding modes in which + IPsec could operate. +

+
@@ -1379,7 +1386,7 @@ - +

A port within a .

Most commonly, a port has exactly one ``interface,'' pointed to by its @@ -2625,7 +2632,7 @@

- 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 true to enable. Checksums present on incoming packets will be validated regardless of this setting. @@ -2643,6 +2650,74 @@ + + +

+ gre, geneve, and + vxlan interfaces support these options. +

+ + +

+ The preshared secret to negotiate tunnel in PSK mode. + Must be unset when peer_cert is set. +

+ +

+ This secret gets populated by ovs-monitor-ipsec daemon + in /etc/ipsec.secrets file. This value must match on both + tunnel ends, because otherwise authentication failure will be + triggered in /var/log/auth.log file by strongSwan. +

+ +
+ + +

+ The public key of remote peer to negotiate tunnel in PKI mode. + Must be unset when psk is set. +

+ +

+ The ovs-monitor-ipsec daemon creates a file with content of this + value under /etc/ipsec.d/certs directory and then references it + from /etc/ipsec.secrets file. This value must be set either to + peer's public certificate contents itself or to the Certificate + Authority of that peer. +

+ +
+ + +

+ The private key of this peer to negotiate tunnel in PKI mode. + This file must never ever be leaked, because anyone who posses + it can negotiate tunnel. + Must be unset when psk is set. +

+ +

+ ... +

+ +
+ + +

+ The public key of this peer to negotiate tunnel in PKI mode. + This file is exhanged over wire during IKE negotiation phase. + Hence it is not a secret. + Must be unset when psk is set. +

+ +

+ ... +

+ +
+ + +