mirror of
https://github.com/openvswitch/ovs
synced 2025-08-22 01:51:26 +00:00
STT and LISP tunnel types were deprecated and marked for removal in the following commits in the OVS 3.5 release: 3b37a6154a59 ("netdev-vport: Deprecate STT tunnel port type.") 8d7ac031c03d ("netdev-vport: Deprecate LISP tunnel port type.") Main reasons were that STT was rejected in upstream kernel and the LISP was never upstreamed as well and doesn't really have a supported implementation. Both protocols also appear to have lost their former relevance. Removing both now. While at it, also fixing some small documentation issues and comments. Acked-by: Eelco Chaudron <echaudro@redhat.com> Acked-by: Alin Serdean <aserdean@ovn.org> Acked-by: Kevin Traynor <ktraynor@redhat.com> Signed-off-by: Ilya Maximets <i.maximets@ovn.org>
1488 lines
54 KiB
Plaintext
Executable File
1488 lines
54 KiB
Plaintext
Executable File
#! @PYTHON3@
|
|
# Copyright (c) 2017 Nicira, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at:
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import argparse
|
|
import copy
|
|
import ipaddress
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from string import Template
|
|
|
|
import ovs.daemon
|
|
import ovs.db.idl
|
|
import ovs.dirs
|
|
import ovs.unixctl
|
|
import ovs.unixctl.server
|
|
import ovs.util
|
|
import ovs.vlog
|
|
|
|
|
|
FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n"
|
|
transp_tmpl = {"gre": Template("""\
|
|
conn $ifname-$version
|
|
$auth_section
|
|
leftprotoport=gre
|
|
rightprotoport=gre
|
|
|
|
"""), "gre64": Template("""\
|
|
conn $ifname-$version
|
|
$auth_section
|
|
leftprotoport=gre
|
|
rightprotoport=gre
|
|
|
|
"""), "geneve": Template("""\
|
|
conn $ifname-in-$version
|
|
$auth_section
|
|
leftprotoport=udp/6081
|
|
rightprotoport=udp
|
|
|
|
conn $ifname-out-$version
|
|
$auth_section
|
|
leftprotoport=udp
|
|
rightprotoport=udp/6081
|
|
|
|
"""), "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
|
|
command_timeout = None
|
|
RECONCILIATION_INTERVAL = 15 # seconds
|
|
TIMEOUT_EXPIRED = 137 # Exit code for a SIGKILL (128 + 9).
|
|
|
|
|
|
def run_command(args, description=None):
|
|
""" This function runs the process args[0] with args[1:] arguments
|
|
and returns a tuple: return-code, stdout, stderr. """
|
|
|
|
if not description:
|
|
description = "run %s command" % args[0]
|
|
|
|
vlog.dbg("Running %s" % args)
|
|
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
try:
|
|
pout, perr = proc.communicate(timeout=command_timeout)
|
|
ret = proc.returncode
|
|
except subprocess.TimeoutExpired:
|
|
vlog.warn("Timed out after %s seconds trying to %s."
|
|
% (command_timeout, description))
|
|
pout, perr = b'', b''
|
|
# Just kill the process here. We can't afford waiting for it,
|
|
# as it may be stuck and may not actually be terminated.
|
|
proc.kill()
|
|
ret = TIMEOUT_EXPIRED
|
|
|
|
if proc.returncode or perr:
|
|
vlog.warn("Failed to %s; exit code: %d"
|
|
% (description, proc.returncode))
|
|
vlog.warn("cmdline: %s" % proc.args)
|
|
vlog.warn("stderr: %s" % perr)
|
|
vlog.warn("stdout: %s" % pout)
|
|
|
|
return ret, pout.decode(), perr.decode()
|
|
|
|
|
|
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 = {}
|
|
|
|
ret, pout, perr = run_command([self.IP, 'xfrm', 'policy'],
|
|
"get XFRM policies")
|
|
if ret:
|
|
return policies
|
|
|
|
for line in pout.splitlines():
|
|
a = line.strip().split(" ")
|
|
if len(a) >= 4 and a[0] == "src" and a[2] == "dst":
|
|
dst = (a[3].split("/"))[0]
|
|
if dst not in policies:
|
|
policies[dst] = []
|
|
policies[dst].append(line)
|
|
src = (a[3].split("/"))[0]
|
|
if src not in policies:
|
|
policies[src] = []
|
|
policies[src].append(line)
|
|
return policies
|
|
|
|
def get_securities(self):
|
|
"""This function returns IPsec security associations (from kernel)
|
|
in a dictionary where <key> is destination IPv4 address and <value>
|
|
is SELECTOR."""
|
|
securities = {}
|
|
|
|
ret, pout, perr = run_command([self.IP, 'xfrm', 'state'],
|
|
"get XFRM state")
|
|
if ret:
|
|
return securities
|
|
|
|
for line in pout.splitlines():
|
|
a = line.strip().split(" ")
|
|
if len(a) >= 4 and a[0] == "sel" \
|
|
and a[1] == "src" and a[3] == "dst":
|
|
remote_ip = a[4].rstrip().split("/")[0]
|
|
local_ip = a[2].rstrip().split("/")[0]
|
|
if remote_ip not in securities:
|
|
securities[remote_ip] = []
|
|
securities[remote_ip].append(line)
|
|
if local_ip not in securities:
|
|
securities[local_ip] = []
|
|
securities[local_ip].append(line)
|
|
return securities
|
|
|
|
|
|
class StrongSwanHelper(object):
|
|
"""This class does StrongSwan specific configurations."""
|
|
|
|
STRONGSWAN_CONF = """%s
|
|
charon {
|
|
plugins {
|
|
kernel-netlink {
|
|
set_proto_port_transport_sa = yes
|
|
xfrm_ack_expires = 10
|
|
}
|
|
gcm {
|
|
load = yes
|
|
}
|
|
}
|
|
load_modular = yes
|
|
}
|
|
""" % (FILE_HEADER)
|
|
|
|
CONF_HEADER = """%s
|
|
config setup
|
|
uniqueids=yes
|
|
|
|
conn %%default
|
|
keyingtries=%%forever
|
|
type=transport
|
|
keyexchange=ikev2
|
|
auto=route
|
|
ike=aes256gcm16-sha256-modp2048
|
|
esp=aes256gcm16-modp2048
|
|
|
|
""" % (FILE_HEADER)
|
|
|
|
CA_SECTION = """ca ca_auth
|
|
cacert=%s
|
|
|
|
"""
|
|
|
|
SHUNT_POLICY = """conn prevent_unencrypted_gre
|
|
type=drop
|
|
leftprotoport=gre
|
|
mark={0}
|
|
|
|
conn prevent_unencrypted_geneve
|
|
type=drop
|
|
leftprotoport=udp/6081
|
|
mark={0}
|
|
|
|
conn prevent_unencrypted_vxlan
|
|
type=drop
|
|
leftprotoport=udp/4789
|
|
mark={0}
|
|
|
|
"""
|
|
|
|
auth_tmpl = {"psk": Template("""\
|
|
left=%any
|
|
right=$remote_ip
|
|
authby=psk"""),
|
|
"pki_remote": Template("""\
|
|
left=%any
|
|
right=$remote_ip
|
|
leftid=$local_name
|
|
rightid=$remote_name
|
|
leftcert=$certificate
|
|
rightcert=$remote_cert"""),
|
|
"pki_ca": Template("""\
|
|
left=%any
|
|
right=$remote_ip
|
|
leftid=$local_name
|
|
rightid=$remote_name
|
|
leftcert=$certificate""")}
|
|
|
|
def __init__(self, root_prefix):
|
|
self.CHARON_CONF = root_prefix + "/etc/strongswan.d/ovs.conf"
|
|
self.IPSEC = root_prefix + "/usr/sbin/ipsec"
|
|
self.IPSEC_CONF = root_prefix + "/etc/ipsec.conf"
|
|
self.IPSEC_SECRETS = root_prefix + "/etc/ipsec.secrets"
|
|
self.conf_file = None
|
|
self.secrets_file = None
|
|
|
|
def restart_ike_daemon(self):
|
|
"""This function restarts StrongSwan."""
|
|
f = open(self.CHARON_CONF, "w")
|
|
f.write(self.STRONGSWAN_CONF)
|
|
f.close()
|
|
|
|
f = open(self.IPSEC_CONF, "w")
|
|
f.write(self.CONF_HEADER)
|
|
f.close()
|
|
|
|
f = open(self.IPSEC_SECRETS, "w")
|
|
f.write(FILE_HEADER)
|
|
f.close()
|
|
|
|
vlog.info("Restarting StrongSwan")
|
|
run_command([self.IPSEC, "restart"], "restart StrongSwan")
|
|
|
|
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 = {}
|
|
ret, pout, perr = run_command([self.IPSEC, 'status'],
|
|
"get active connections")
|
|
if ret:
|
|
return conns
|
|
|
|
for line in pout.splitlines():
|
|
tunnel_name = line.strip().split(":")
|
|
if len(tunnel_name) < 2:
|
|
continue
|
|
m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+).*", tunnel_name[0])
|
|
if not m:
|
|
continue
|
|
ifname = m.group(1)
|
|
if ifname not in conns:
|
|
conns[ifname] = {}
|
|
(conns[ifname])[tunnel_name[0]] = line
|
|
|
|
return conns
|
|
|
|
def need_to_reconcile(self, monitor):
|
|
return False
|
|
|
|
def config_init(self):
|
|
self.conf_file = open(self.IPSEC_CONF, "w")
|
|
self.secrets_file = open(self.IPSEC_SECRETS, "w")
|
|
self.conf_file.write(self.CONF_HEADER)
|
|
self.secrets_file.write(FILE_HEADER)
|
|
|
|
def config_global(self, monitor):
|
|
"""Configure the global state of IPsec tunnels."""
|
|
needs_refresh = False
|
|
|
|
if monitor.conf_in_use != monitor.conf:
|
|
monitor.conf_in_use = copy.deepcopy(monitor.conf)
|
|
needs_refresh = True
|
|
|
|
# Configure the shunt policy
|
|
if monitor.conf_in_use["skb_mark"]:
|
|
skb_mark = monitor.conf_in_use["skb_mark"]
|
|
self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
|
|
|
|
# Configure the CA cert
|
|
if monitor.conf_in_use["pki"]["ca_cert"]:
|
|
cacert = monitor.conf_in_use["pki"]["ca_cert"]
|
|
self.conf_file.write(self.CA_SECTION % cacert)
|
|
|
|
return needs_refresh
|
|
|
|
def config_tunnel(self, tunnel):
|
|
if tunnel.conf["psk"]:
|
|
self.secrets_file.write('%%any %s : PSK "%s"\n' %
|
|
(tunnel.conf["remote_ip"], tunnel.conf["psk"]))
|
|
auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
|
|
else:
|
|
self.secrets_file.write("%%any %s : RSA %s\n" %
|
|
(tunnel.conf["remote_ip"],
|
|
tunnel.conf["private_key"]))
|
|
if tunnel.conf["remote_cert"]:
|
|
tmpl = self.auth_tmpl["pki_remote"]
|
|
auth_section = tmpl.substitute(tunnel.conf)
|
|
else:
|
|
tmpl = self.auth_tmpl["pki_ca"]
|
|
auth_section = tmpl.substitute(tunnel.conf)
|
|
|
|
if "custom_options" in tunnel.conf:
|
|
for key, value in tunnel.conf["custom_options"].items():
|
|
auth_section += "\n " + key + "=" + value
|
|
|
|
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")
|
|
|
|
run_command([self.IPSEC, "update"],
|
|
"update StrongSwan's configuration")
|
|
run_command([self.IPSEC, "rereadsecrets"], "re-read secrets")
|
|
|
|
# "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.items():
|
|
tunnel = monitor.tunnels.get(ifname)
|
|
for conn in conns:
|
|
# IPsec "connection" names that we choose in strongswan
|
|
# must start with Interface name
|
|
if not conn.startswith(ifname):
|
|
vlog.err("%s does not start with %s" % (conn, ifname))
|
|
continue
|
|
|
|
# version number should be the first integer after
|
|
# interface name in IPsec "connection"
|
|
try:
|
|
ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
|
|
except IndexError:
|
|
vlog.err("%s does not contain version number")
|
|
continue
|
|
except ValueError:
|
|
vlog.err("%s does not contain version number")
|
|
continue
|
|
|
|
if not tunnel or tunnel.version != ver:
|
|
vlog.info("%s is outdated %u" % (conn, ver))
|
|
run_command([self.IPSEC, "stroke", "down-nb", conn],
|
|
"stroke the outdated %s" % conn)
|
|
|
|
|
|
class LibreSwanHelper(object):
|
|
"""This class does LibreSwan specific configurations."""
|
|
CONF_DEFAULT_HEADER = """\
|
|
config setup
|
|
uniqueids=yes
|
|
|
|
conn %default
|
|
"""
|
|
|
|
CONN_CONF_BASE = """\
|
|
keyingtries=%forever
|
|
type=transport
|
|
auto=route
|
|
"""
|
|
|
|
CONN_CONF_CRYPTO = """\
|
|
ike=aes_gcm256-sha2_256
|
|
esp=aes_gcm256
|
|
ikev2=insist
|
|
"""
|
|
|
|
SHUNT_POLICY = """conn prevent_unencrypted_gre
|
|
type=drop
|
|
left=%defaultroute
|
|
leftprotoport=gre
|
|
mark={0}
|
|
|
|
conn prevent_unencrypted_geneve
|
|
type=drop
|
|
left=%defaultroute
|
|
leftprotoport=udp/6081
|
|
mark={0}
|
|
|
|
conn prevent_unencrypted_vxlan
|
|
type=drop
|
|
left=%defaultroute
|
|
leftprotoport=udp/4789
|
|
mark={0}
|
|
|
|
"""
|
|
|
|
IPV6_CONN = """\
|
|
hostaddrfamily=ipv6
|
|
clientaddrfamily=ipv6
|
|
"""
|
|
|
|
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="ovs_certkey_$local_name"
|
|
rightcert="ovs_cert_$remote_name"
|
|
leftrsasigkey=%cert"""),
|
|
"pki_ca": Template("""\
|
|
left=$local_ip
|
|
right=$remote_ip
|
|
leftid=@$local_name
|
|
rightid=@$remote_name
|
|
leftcert="ovs_certkey_$local_name"
|
|
leftrsasigkey=%cert
|
|
rightca=%same""")}
|
|
|
|
CERT_PREFIX = "ovs_cert_"
|
|
CERTKEY_PREFIX = "ovs_certkey_"
|
|
|
|
def __init__(self, libreswan_root_prefix, args):
|
|
# Collect version infromation
|
|
self.IPSEC = libreswan_root_prefix + "/usr/sbin/ipsec"
|
|
self.IPSEC_AUTO = [self.IPSEC]
|
|
|
|
ret, pout, perr = run_command([self.IPSEC, "--version"],
|
|
"get Libreswan's version")
|
|
try:
|
|
v = re.match("^Libreswan v?(.*)$", pout.strip())
|
|
version = int(v.group(1).split(".")[0])
|
|
except:
|
|
version = 0
|
|
|
|
if version < 5:
|
|
# With v5, LibreSWAN removed the auto command, however, it is
|
|
# still required for older versions
|
|
self.IPSEC_AUTO.append("auto")
|
|
|
|
if version >= 4:
|
|
ipsec_d = args.ipsec_d if args.ipsec_d else "/var/lib/ipsec/nss"
|
|
else:
|
|
ipsec_d = args.ipsec_d if args.ipsec_d else "/etc/ipsec.d"
|
|
|
|
ipsec_conf = args.ipsec_conf if args.ipsec_conf else "/etc/ipsec.conf"
|
|
ipsec_secrets = (args.ipsec_secrets if args.ipsec_secrets
|
|
else "/etc/ipsec.secrets")
|
|
ipsec_ctl = (args.ipsec_ctl if args.ipsec_ctl
|
|
else "/run/pluto/pluto.ctl")
|
|
|
|
self.IPSEC_CONF = libreswan_root_prefix + ipsec_conf
|
|
self.ROOT_IPSEC_CONF = self.IPSEC_CONF
|
|
if args.root_ipsec_conf:
|
|
self.ROOT_IPSEC_CONF = args.root_ipsec_conf
|
|
self.IPSEC_SECRETS = libreswan_root_prefix + ipsec_secrets
|
|
self.IPSEC_D = "sql:" + libreswan_root_prefix + ipsec_d
|
|
self.IPSEC_CTL = libreswan_root_prefix + ipsec_ctl
|
|
self.conf_file = None
|
|
self.conns_not_active = set()
|
|
self.last_refresh = time.time()
|
|
self.secrets_file = None
|
|
self.use_default_conn = self.IPSEC_CONF == self.ROOT_IPSEC_CONF
|
|
self.use_default_crypto = args.use_default_crypto
|
|
vlog.dbg("Using: " + self.IPSEC)
|
|
vlog.dbg("Configuration file: " + self.IPSEC_CONF)
|
|
vlog.dbg("Root configuration file: " + self.ROOT_IPSEC_CONF)
|
|
vlog.dbg("Secrets file: " + self.IPSEC_SECRETS)
|
|
vlog.dbg("ipsec.d: " + self.IPSEC_D)
|
|
vlog.dbg("Pluto socket: " + self.IPSEC_CTL)
|
|
|
|
def restart_ike_daemon(self):
|
|
"""This function restarts LibreSwan."""
|
|
# Remove the stale information from the NSS database
|
|
self._nss_clear_database()
|
|
|
|
f = open(self.IPSEC_CONF, "w")
|
|
f.write(FILE_HEADER)
|
|
if self.use_default_conn:
|
|
f.write(self.CONF_DEFAULT_HEADER)
|
|
f.write(self.CONN_CONF_BASE)
|
|
if not self.use_default_crypto:
|
|
f.write(self.CONN_CONF_CRYPTO)
|
|
f.write("\n")
|
|
f.close()
|
|
|
|
f = open(self.IPSEC_SECRETS, "w")
|
|
f.write(FILE_HEADER)
|
|
f.close()
|
|
|
|
vlog.info("Restarting LibreSwan")
|
|
run_command([self.IPSEC, "restart"], "restart Libreswan")
|
|
|
|
def config_init(self):
|
|
self.conf_file = open(self.IPSEC_CONF, "w")
|
|
self.secrets_file = open(self.IPSEC_SECRETS, "w")
|
|
self.conf_file.write(FILE_HEADER)
|
|
if self.use_default_conn:
|
|
self.conf_file.write(self.CONF_DEFAULT_HEADER)
|
|
self.conf_file.write(self.CONN_CONF_BASE)
|
|
if not self.use_default_crypto:
|
|
self.conf_file.write(self.CONN_CONF_CRYPTO)
|
|
self.conf_file.write("\n")
|
|
|
|
self.secrets_file.write(FILE_HEADER)
|
|
|
|
def config_global(self, monitor):
|
|
"""Configure the global state of IPsec tunnels."""
|
|
needs_refresh = False
|
|
|
|
if monitor.conf_in_use["pki"] != monitor.conf["pki"]:
|
|
# Clear old state
|
|
if monitor.conf_in_use["pki"]["certificate"]:
|
|
local_name = monitor.conf_in_use["pki"]["local_name"]
|
|
self._nss_delete_cert_and_key(self.CERTKEY_PREFIX + local_name)
|
|
|
|
if monitor.conf_in_use["pki"]["ca_cert"]:
|
|
self._nss_delete_cert(self.CERT_PREFIX + "cacert")
|
|
|
|
# Load new state
|
|
if monitor.conf["pki"]["certificate"]:
|
|
cert = monitor.conf["pki"]["certificate"]
|
|
key = monitor.conf["pki"]["private_key"]
|
|
name = monitor.conf["pki"]["local_name"]
|
|
name = self.CERTKEY_PREFIX + name
|
|
self._nss_import_cert_and_key(cert, key, name)
|
|
|
|
if monitor.conf["pki"]["ca_cert"]:
|
|
self._nss_import_cert(monitor.conf["pki"]["ca_cert"],
|
|
self.CERT_PREFIX + "cacert", 'CT,,')
|
|
|
|
monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"])
|
|
needs_refresh = True
|
|
|
|
# Configure the shunt policy
|
|
if monitor.conf["skb_mark"]:
|
|
skb_mark = monitor.conf["skb_mark"]
|
|
self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
|
|
|
|
# Will update conf_in_use later in the 'refresh' method
|
|
if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
|
|
needs_refresh = True
|
|
|
|
return needs_refresh
|
|
|
|
def config_tunnel(self, tunnel):
|
|
if tunnel.conf["psk"]:
|
|
self.secrets_file.write('%%any %s : PSK "%s"\n' %
|
|
(tunnel.conf["remote_ip"], tunnel.conf["psk"]))
|
|
auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
|
|
elif tunnel.conf["remote_cert"]:
|
|
auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)
|
|
self._nss_import_cert(tunnel.conf["remote_cert"],
|
|
self.CERT_PREFIX + tunnel.conf["remote_name"],
|
|
'P,P,P')
|
|
else:
|
|
auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf)
|
|
|
|
if tunnel.conf["address_family"] == "IPv6":
|
|
auth_section = self.IPV6_CONN + auth_section
|
|
|
|
if not self.use_default_conn:
|
|
auth_section = self.CONN_CONF_BASE + auth_section
|
|
if not self.use_default_crypto:
|
|
auth_section = self.CONN_CONF_CRYPTO + auth_section
|
|
|
|
if "custom_options" in tunnel.conf:
|
|
for key, value in tunnel.conf["custom_options"].items():
|
|
auth_section += "\n " + key + "=" + value
|
|
|
|
vals = tunnel.conf.copy()
|
|
vals["auth_section"] = auth_section
|
|
vals["version"] = tunnel.version
|
|
conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals)
|
|
self.conf_file.write(conf_text)
|
|
|
|
def config_fini(self):
|
|
self.secrets_file.close()
|
|
self.conf_file.close()
|
|
self.secrets_file = None
|
|
self.conf_file = None
|
|
|
|
def clear_tunnel_state(self, tunnel):
|
|
if tunnel.conf["remote_cert"]:
|
|
name = self.CERT_PREFIX + tunnel.conf["remote_name"]
|
|
self._nss_delete_cert(name)
|
|
|
|
def refresh(self, monitor):
|
|
vlog.info("Refreshing LibreSwan configuration")
|
|
run_command(self.IPSEC_AUTO + ["--ctlsocket", self.IPSEC_CTL,
|
|
"--config", self.ROOT_IPSEC_CONF,
|
|
"--rereadsecrets"],
|
|
"re-read secrets")
|
|
|
|
loaded_conns = self.get_loaded_conns()
|
|
active_conns = self.get_active_conns()
|
|
|
|
all_names = set(monitor.tunnels.keys()) | \
|
|
set(loaded_conns.keys()) | \
|
|
set(active_conns.keys())
|
|
|
|
for name in all_names:
|
|
desired = set(self.get_conn_names(monitor, name))
|
|
loaded = set(loaded_conns.get(name, dict()).keys())
|
|
active = set(active_conns.get(name, dict()).keys())
|
|
|
|
# Untrack connections that became active.
|
|
self.conns_not_active.difference_update(active)
|
|
# Remove connections that didn't become active after --start
|
|
# and another explicit --up.
|
|
for conn in self.conns_not_active & loaded:
|
|
self._delete_ipsec_connection(conn, "is defunct")
|
|
loaded.remove(conn)
|
|
|
|
# Remove all the loaded or active but not desired connections.
|
|
for conn in loaded | active:
|
|
if conn not in desired:
|
|
self._delete_ipsec_connection(conn, "is outdated")
|
|
loaded.discard(conn)
|
|
active.discard(conn)
|
|
|
|
# If not all desired are loaded, remove all the loaded and
|
|
# active for this tunnel and re-load only the desired ones.
|
|
# Need to do that, because connections for the same tunnel
|
|
# may share SAs. If one is loaded and the other is not,
|
|
# it means the second one failed, so the shared SA may be in
|
|
# a broken state.
|
|
if desired != loaded:
|
|
for conn in loaded | active:
|
|
self._delete_ipsec_connection(conn, "is half-loaded")
|
|
loaded.discard(conn)
|
|
active.discard(conn)
|
|
|
|
for conn in desired:
|
|
# Start (add + up) outgoing connections and only add
|
|
# incoming ones. If the other side will not initiate
|
|
# the connection and it will not become active, we'll
|
|
# bring it up during the next refresh.
|
|
if re.match(r".*-in-\d+$", conn):
|
|
vlog.info("Adding ipsec connection %s" % conn)
|
|
self._start_ipsec_connection(conn, "add")
|
|
else:
|
|
vlog.info("Starting ipsec connection %s" % conn)
|
|
self._start_ipsec_connection(conn, "start")
|
|
else:
|
|
# Ask pluto to bring UP connections that are loaded,
|
|
# but not active for some reason.
|
|
#
|
|
# desired == loaded and desired >= loaded + active,
|
|
# so loaded >= active
|
|
for conn in loaded - active:
|
|
vlog.info("Bringing up ipsec connection %s" % conn)
|
|
# On failure to --up it will be removed from the set.
|
|
self.conns_not_active.add(conn)
|
|
self._start_ipsec_connection(conn, "up")
|
|
|
|
# Update shunt policy if changed
|
|
if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
|
|
if monitor.conf["skb_mark"]:
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--add",
|
|
"--asynchronous", "prevent_unencrypted_gre"])
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--add",
|
|
"--asynchronous", "prevent_unencrypted_geneve"])
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--add",
|
|
"--asynchronous", "prevent_unencrypted_vxlan"])
|
|
else:
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--delete",
|
|
"--asynchronous", "prevent_unencrypted_gre"])
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--delete",
|
|
"--asynchronous", "prevent_unencrypted_geneve"])
|
|
run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--delete",
|
|
"--asynchronous", "prevent_unencrypted_vxlan"])
|
|
monitor.conf_in_use["skb_mark"] = monitor.conf["skb_mark"]
|
|
self.last_refresh = time.time()
|
|
vlog.info("Refreshing is done.")
|
|
|
|
def get_conns_from_status(self, pattern):
|
|
"""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>. 'pattern' should
|
|
be a regular expression that parses out the connection name.
|
|
Only the lines that match the pattern will be parsed. """
|
|
|
|
conns = {}
|
|
ret, pout, perr = run_command([self.IPSEC, 'status',
|
|
'--ctlsocket', self.IPSEC_CTL],
|
|
"get ipsec status")
|
|
if ret:
|
|
return conns
|
|
|
|
for line in pout.splitlines():
|
|
m = re.search(pattern, line)
|
|
if not m:
|
|
continue
|
|
|
|
conn = m.group(1)
|
|
m = re.match(r"(.*)(-in-\d+|-out-\d+)", conn)
|
|
if not m:
|
|
# GRE connections have format <iface>-<ver>
|
|
m = re.match(r"(.*)(-\d+)", conn)
|
|
if not m:
|
|
continue
|
|
|
|
ifname = m.group(1)
|
|
if ifname not in conns:
|
|
conns[ifname] = {}
|
|
(conns[ifname])[conn] = line
|
|
|
|
return conns
|
|
|
|
def get_active_conns(self):
|
|
return self.get_conns_from_status(r"#\d+: .*\"(.*)\".*")
|
|
|
|
def get_loaded_conns(self):
|
|
return self.get_conns_from_status(r"\"(.*)\": \d+.*(===|\.\.\.).*")
|
|
|
|
def get_conn_names(self, monitor, ifname):
|
|
conns = []
|
|
if ifname not in monitor.tunnels:
|
|
return conns
|
|
|
|
tunnel = monitor.tunnels.get(ifname)
|
|
ver = tunnel.version
|
|
|
|
if tunnel.conf["tunnel_type"] == "gre":
|
|
conns.append("%s-%s" % (ifname, ver))
|
|
else:
|
|
conns.append("%s-in-%s" % (ifname, ver))
|
|
conns.append("%s-out-%s" % (ifname, ver))
|
|
|
|
return conns
|
|
|
|
def need_to_reconcile(self, monitor):
|
|
if time.time() - self.last_refresh < RECONCILIATION_INTERVAL:
|
|
return False
|
|
|
|
conns_dict = self.get_active_conns()
|
|
for ifname, tunnel in monitor.tunnels.items():
|
|
if ifname not in conns_dict:
|
|
vlog.info("Connection for port %s is not active, "
|
|
"need to reconcile" % ifname)
|
|
return True
|
|
|
|
existing_conns = conns_dict.get(ifname)
|
|
desired_conns = self.get_conn_names(monitor, ifname)
|
|
|
|
if set(existing_conns.keys()) != set(desired_conns):
|
|
vlog.info("Active connections for port %s %s do not match "
|
|
"desired %s, need to reconcile"
|
|
% (ifname, list(existing_conns.keys()),
|
|
desired_conns))
|
|
return True
|
|
|
|
return False
|
|
|
|
def _delete_ipsec_connection(self, conn, reason):
|
|
vlog.info("%s %s, removing" % (conn, reason))
|
|
self.conns_not_active.discard(conn)
|
|
run_command(self.IPSEC_AUTO +
|
|
["--ctlsocket", self.IPSEC_CTL,
|
|
"--config", self.ROOT_IPSEC_CONF,
|
|
"--delete", conn], "delete %s" % conn)
|
|
|
|
def _start_ipsec_connection(self, conn, action):
|
|
asynchronous = [] if action == "add" else ["--asynchronous"]
|
|
ret, pout, perr = run_command(self.IPSEC_AUTO +
|
|
["--config", self.ROOT_IPSEC_CONF,
|
|
"--ctlsocket", self.IPSEC_CTL,
|
|
"--" + action,
|
|
*asynchronous, conn],
|
|
"%s %s" % (action, conn))
|
|
|
|
if re.match(r".*[F|f]ailed to initiate connection.*", pout):
|
|
vlog.err('Failed to initiate connection through'
|
|
' Interface %s.\n' % (conn.split('-')[0]))
|
|
vlog.err("stdout: %s" % pout)
|
|
ret = 1
|
|
|
|
if ret:
|
|
# We don't know in which state the connection was left on
|
|
# failure. Try to clean it up.
|
|
self._delete_ipsec_connection(conn, "--%s failed" % action)
|
|
|
|
def _nss_clear_database(self):
|
|
"""Remove all OVS IPsec related state from the NSS database"""
|
|
ret, pout, perr = run_command(['certutil', '-L', '-d', self.IPSEC_D],
|
|
"clear NSS database")
|
|
if ret:
|
|
return
|
|
|
|
for line in pout.splitlines():
|
|
s = line.strip().split()
|
|
if len(s) < 1:
|
|
continue
|
|
name = s[0]
|
|
if name.startswith(self.CERT_PREFIX):
|
|
self._nss_delete_cert(name)
|
|
elif name.startswith(self.CERTKEY_PREFIX):
|
|
self._nss_delete_cert_and_key(name)
|
|
|
|
def _nss_import_cert(self, cert, name, cert_type):
|
|
"""Cert_type is 'CT,,' for the CA certificate and 'P,P,P' for the
|
|
normal certificate."""
|
|
run_command(['certutil', '-A', '-a', '-i', cert, '-d', self.IPSEC_D,
|
|
'-n', name, '-t', cert_type],
|
|
"import certificate %s into NSS" % name)
|
|
|
|
def _nss_delete_cert(self, name):
|
|
run_command(['certutil', '-D', '-d', self.IPSEC_D, '-n', name],
|
|
"delete certificate %s from NSS" % name)
|
|
|
|
def _nss_import_cert_and_key(self, cert, key, name):
|
|
# Avoid deleting other files
|
|
path = os.path.abspath('/tmp/%s.p12' % name)
|
|
if not path.startswith('/tmp/'):
|
|
vlog.err("Illegal certificate name '%s'!" % name)
|
|
return
|
|
|
|
if run_command(['openssl', 'pkcs12', '-export',
|
|
'-in', cert, '-inkey', key,
|
|
'-out', path, '-name', name,
|
|
'-passout', 'pass:'],
|
|
"create p12 file from pem files")[0]:
|
|
return
|
|
|
|
# Load p12 file to the database
|
|
run_command(['pk12util', '-i', path, '-d', self.IPSEC_D, '-W', ''],
|
|
"load p12 file to the NSS database")
|
|
os.remove(path)
|
|
|
|
def _nss_delete_cert_and_key(self, name):
|
|
# Delete certificate and private key
|
|
run_command(['certutil', '-F', '-d', self.IPSEC_D, '-n', name],
|
|
"delete certificate and private key for %s" % 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
|
|
Address Family: $address_family
|
|
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
|
|
Custom Options: $custom_options
|
|
""")
|
|
|
|
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,
|
|
"local_ip": options.get("local_ip", "%defaultroute"),
|
|
"remote_ip": options.get("remote_ip"),
|
|
"address_family": self._get_conn_address_family(
|
|
options.get("remote_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"),
|
|
"custom_options": {}}
|
|
|
|
# add custom ipsec options to the connection
|
|
for key, value in options.items():
|
|
if key.startswith("ipsec_"):
|
|
new_conf["custom_options"][key[len("ipsec_"):]] = value
|
|
|
|
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 _get_conn_address_family(self, remote_ip, local_ip):
|
|
remote = address_family(remote_ip)
|
|
local = address_family(local_ip)
|
|
|
|
if local is None:
|
|
return remote
|
|
elif local != remote:
|
|
return None
|
|
else:
|
|
return remote
|
|
|
|
def _is_valid_tunnel_conf(self):
|
|
"""This function verifies if IPsec tunnel has valid configuration
|
|
set in 'conf'. If it is valid, then it returns True. Otherwise,
|
|
it returns False and sets the reason why configuration was considered
|
|
as invalid.
|
|
|
|
This function could be improved in future to also verify validness
|
|
of certificates themselves so that ovs-monitor-ipsec would not
|
|
pass malformed configuration to IKE daemon."""
|
|
|
|
self.invalid_reason = None
|
|
|
|
if not self.conf["remote_ip"]:
|
|
self.invalid_reason = ": 'remote_ip' is not set"
|
|
return False
|
|
|
|
if self.conf["psk"]:
|
|
if self.conf["certificate"] or self.conf["private_key"] \
|
|
or self.conf["ca_cert"] or self.conf["remote_cert"] \
|
|
or self.conf["remote_name"]:
|
|
self.invalid_reason = ": 'certificate', 'private_key', "\
|
|
"'ca_cert', 'remote_cert', and "\
|
|
"'remote_name' must be unset with PSK"
|
|
return False
|
|
# If configuring authentication with CA-signed certificate or
|
|
# self-signed certificate, the 'remote_name' should be specified at
|
|
# this point. When using CA-signed certificate, the 'remote_name' is
|
|
# read from interface's options field. When using self-signed
|
|
# certificate, the 'remote_name' is extracted from the 'remote_cert'
|
|
# file.
|
|
elif self.conf["remote_name"]:
|
|
if not self.conf["certificate"]:
|
|
self.invalid_reason = ": must set 'certificate' as local"\
|
|
" certificate when using CA-signed"\
|
|
" certificate or self-signed"\
|
|
" certificate to authenticate peers"
|
|
return False
|
|
elif not self.conf["private_key"]:
|
|
self.invalid_reason = ": must set 'private_key' as local"\
|
|
" private key when using CA-signed"\
|
|
" certificate or self-signed"\
|
|
" certificate to authenticate peers"
|
|
return False
|
|
if not self.conf["remote_cert"] and not self.conf["ca_cert"]:
|
|
self.invalid_reason = ": must set 'remote_cert' when using"\
|
|
" self-signed certificate"\
|
|
" authentication or 'ca_cert' when"\
|
|
" using CA-signed certificate"\
|
|
" authentication"
|
|
return False
|
|
else:
|
|
self.invalid_reason = ": must choose a authentication method"
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class IPsecMonitor(object):
|
|
"""This class monitors and configures IPsec tunnels"""
|
|
|
|
def __init__(self, root_prefix, ike_daemon, restart, args):
|
|
self.IPSEC = root_prefix + "/usr/sbin/ipsec"
|
|
self.tunnels = {}
|
|
|
|
# Global configuration shared by all tunnels
|
|
self.conf = {
|
|
"pki": {
|
|
"private_key": None,
|
|
"certificate": None,
|
|
"ca_cert": None,
|
|
"local_name": None
|
|
},
|
|
"skb_mark": None
|
|
}
|
|
self.conf_in_use = copy.deepcopy(self.conf)
|
|
|
|
# Choose to either use StrongSwan or LibreSwan as IKE daemon
|
|
if ike_daemon == "strongswan":
|
|
self.ike_helper = StrongSwanHelper(root_prefix)
|
|
elif ike_daemon == "libreswan":
|
|
self.ike_helper = LibreSwanHelper(root_prefix, args)
|
|
else:
|
|
vlog.err("The IKE daemon should be strongswan or libreswan.")
|
|
sys.exit(1)
|
|
|
|
# Check whether ipsec command is available
|
|
if not os.path.isfile(self.IPSEC) or \
|
|
not os.access(self.IPSEC, os.X_OK):
|
|
vlog.err("IKE daemon is not installed in the system.")
|
|
|
|
if restart:
|
|
vlog.info("Restarting IKE daemon")
|
|
self.ike_helper.restart_ike_daemon()
|
|
|
|
def is_tunneling_type_supported(self, tunnel_type):
|
|
"""Returns True if we know how to configure IPsec for these
|
|
types of tunnels. Otherwise, returns False."""
|
|
return tunnel_type in ["gre", "geneve", "vxlan"]
|
|
|
|
def is_ipsec_required(self, options_column):
|
|
"""Return True if tunnel needs to be encrypted. Otherwise,
|
|
returns False."""
|
|
return "psk" in options_column or \
|
|
"remote_name" in options_column or \
|
|
"remote_cert" in options_column
|
|
|
|
def add_tunnel(self, name, row):
|
|
"""Adds a new tunnel that monitor will provision with 'name'."""
|
|
vlog.info("Tunnel %s appeared in OVSDB" % (name))
|
|
self.tunnels[name] = IPsecTunnel(name, row)
|
|
|
|
def update_tunnel(self, name, row):
|
|
"""Updates configuration of already existing tunnel with 'name'."""
|
|
tunnel = self.tunnels[name]
|
|
if tunnel.update_conf(row):
|
|
vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" %
|
|
(tunnel.name, tunnel.version))
|
|
|
|
def del_tunnel(self, name):
|
|
"""Deletes tunnel by 'name'."""
|
|
vlog.info("Tunnel %s disappeared from OVSDB" % (name))
|
|
self.tunnels[name].mark_for_removal()
|
|
|
|
def update_conf(self, pki, skb_mark):
|
|
"""Update the global configuration for IPsec tunnels"""
|
|
self.conf["pki"]["certificate"] = pki[0]
|
|
self.conf["pki"]["private_key"] = pki[1]
|
|
self.conf["pki"]["ca_cert"] = pki[2]
|
|
self.conf["pki"]["local_name"] = pki[3]
|
|
|
|
# Update skb_mark used in IPsec policies.
|
|
self.conf["skb_mark"] = skb_mark
|
|
|
|
def read_ovsdb_open_vswitch_table(self, data):
|
|
"""This functions reads IPsec relevant configuration from Open_vSwitch
|
|
table."""
|
|
pki = [None, None, None, None]
|
|
skb_mark = None
|
|
is_valid = False
|
|
|
|
for row in data["Open_vSwitch"].rows.values():
|
|
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.values():
|
|
if not self.is_tunneling_type_supported(row.type):
|
|
continue
|
|
if not self.is_ipsec_required(row.options):
|
|
continue
|
|
if row.name in self.tunnels:
|
|
self.update_tunnel(row.name, row)
|
|
else:
|
|
self.add_tunnel(row.name, row)
|
|
ifaces.add(row.name)
|
|
|
|
# Mark for removal those tunnels that just disappeared from OVSDB
|
|
for tunnel in self.tunnels.keys():
|
|
if tunnel not in ifaces:
|
|
self.del_tunnel(tunnel)
|
|
|
|
def read_ovsdb(self, data):
|
|
"""This function reads all configuration from OVSDB that
|
|
ovs-monitor-ipsec is interested in."""
|
|
self.read_ovsdb_open_vswitch_table(data)
|
|
self.read_ovsdb_interface_table(data)
|
|
|
|
def show(self, unix_conn, policies, securities):
|
|
"""This function prints all tunnel state in 'unix_conn'.
|
|
It uses 'policies' and securities' received from Linux Kernel
|
|
to show if tunnels were actually configured by the IKE deamon."""
|
|
if not self.tunnels:
|
|
unix_conn.reply("No tunnels configured with IPsec")
|
|
return
|
|
s = ""
|
|
conns = self.ike_helper.get_active_conns()
|
|
for name, tunnel in self.tunnels.items():
|
|
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.items():
|
|
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 or self.ike_helper.need_to_reconcile(self):
|
|
self.ike_helper.refresh(self)
|
|
|
|
def _get_cn_from_cert(self, cert):
|
|
ret, pout, perr = run_command(['openssl', 'x509', '-noout', '-subject',
|
|
'-nameopt', 'RFC2253', '-in', cert],
|
|
"get certificate %s options" % cert)
|
|
if ret:
|
|
return None
|
|
|
|
m = re.search(r"CN=(.+?),", pout.strip())
|
|
if not m:
|
|
vlog.warn("No CN in the certificate subject (%s)." % cert)
|
|
return None
|
|
|
|
return m.group(1)
|
|
|
|
|
|
def address_family(address):
|
|
try:
|
|
ip = ipaddress.ip_address(address)
|
|
ipstr = str(type(ip))
|
|
# ipaddress has inconsistencies with what exceptions are raised:
|
|
# https://mail.openvswitch.org/pipermail/ovs-dev/2021-April/381696.html
|
|
except (ValueError, ipaddress.AddressValueError):
|
|
return None
|
|
if ipstr.find('v6') != -1:
|
|
return "IPv6"
|
|
return "IPv4"
|
|
|
|
|
|
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, argv, unused_aux):
|
|
global monitor
|
|
global exiting
|
|
ret = None
|
|
exiting = True
|
|
cleanup = True
|
|
|
|
for arg in argv:
|
|
if arg == "--no-cleanup":
|
|
cleanup = False
|
|
else:
|
|
cleanup = False
|
|
exiting = False
|
|
ret = str("unrecognized parameter: %s" % arg)
|
|
|
|
if cleanup:
|
|
# 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(ret)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("database", metavar="DATABASE",
|
|
help="A socket on which ovsdb-server is listening.")
|
|
parser.add_argument("--root-prefix", metavar="DIR",
|
|
help="Use DIR as alternate root directory"
|
|
" (for testing).")
|
|
parser.add_argument("--ike-daemon", metavar="IKE-DAEMON",
|
|
help="The IKE daemon used for IPsec tunnels"
|
|
" (either libreswan or strongswan).")
|
|
parser.add_argument("--no-restart-ike-daemon", action='store_true',
|
|
help="Don't restart the IKE daemon on startup.")
|
|
parser.add_argument("--ipsec-conf", metavar="IPSEC-CONF",
|
|
help="Use DIR/IPSEC-CONF as location for "
|
|
" ipsec.conf to overwrite (libreswan only).")
|
|
parser.add_argument("--root-ipsec-conf", metavar="ROOT-IPSEC-CONF",
|
|
help="The read-only root configuration file that"
|
|
" 'include's the one provided in --ipsec-conf. Will"
|
|
" be used to call ipsec commands (libreswan only).")
|
|
parser.add_argument("--ipsec-d", metavar="IPSEC-D",
|
|
help="Use DIR/IPSEC-D as location for "
|
|
" ipsec.d (libreswan only).")
|
|
parser.add_argument("--ipsec-secrets", metavar="IPSEC-SECRETS",
|
|
help="Use DIR/IPSEC-SECRETS as location for "
|
|
" ipsec.secrets (libreswan only).")
|
|
parser.add_argument("--ipsec-ctl", metavar="IPSEC-CTL",
|
|
help="Use DIR/IPSEC-CTL as location for "
|
|
" pluto ctl socket (libreswan only).")
|
|
parser.add_argument("--command-timeout", metavar="TIMEOUT",
|
|
type=int, default=120,
|
|
help="Timeout for external commands called by the "
|
|
"ovs-monitor-ipsec daemon, e.g. ipsec --start.")
|
|
parser.add_argument("--use-default-crypto", action='store_true',
|
|
help="Use default crypto configuration, i.e. do"
|
|
" not set ike/esp/ikev2 in connections"
|
|
" (libreswan only)")
|
|
|
|
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
|
|
global command_timeout
|
|
|
|
root_prefix = args.root_prefix if args.root_prefix else ""
|
|
xfrm = XFRM(root_prefix)
|
|
monitor = IPsecMonitor(root_prefix, args.ike_daemon,
|
|
not args.no_restart_ike_daemon, args)
|
|
command_timeout = args.command_timeout
|
|
|
|
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("list-commands", "", 0, 0,
|
|
ovs.unixctl._unixctl_help, None)
|
|
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", "[--no-cleanup]", 0, 1,
|
|
unixctl_exit, None)
|
|
|
|
error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
|
|
if error:
|
|
ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
|
|
|
|
# Sequence number when OVSDB was processed last time
|
|
seqno = idl.change_seqno
|
|
|
|
while True:
|
|
unixctl_server.run()
|
|
if exiting:
|
|
break
|
|
|
|
idl.run()
|
|
if seqno != idl.change_seqno:
|
|
monitor.read_ovsdb(idl.tables)
|
|
seqno = idl.change_seqno
|
|
|
|
monitor.run()
|
|
|
|
poller = ovs.poller.Poller()
|
|
unixctl_server.wait(poller)
|
|
idl.wait(poller)
|
|
poller.timer_wait(RECONCILIATION_INTERVAL * 1000)
|
|
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)
|