Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 3.135.247.24
#! /usr/bin/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
from string import Template
import ovs.daemon
import ovs.db.idl
import ovs.dirs
import ovs.unixctl
import ovs.unixctl.server
import ovs.util
import ovs.vlog
FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n"
transp_tmpl = {"gre": Template("""\
conn $ifname-$version
$auth_section
leftprotoport=gre
rightprotoport=gre
"""), "gre64": Template("""\
conn $ifname-$version
$auth_section
leftprotoport=gre
rightprotoport=gre
"""), "geneve": Template("""\
conn $ifname-in-$version
$auth_section
leftprotoport=udp/6081
rightprotoport=udp
conn $ifname-out-$version
$auth_section
leftprotoport=udp
rightprotoport=udp/6081
"""), "stt": Template("""\
conn $ifname-in-$version
$auth_section
leftprotoport=tcp/7471
rightprotoport=tcp
conn $ifname-out-$version
$auth_section
leftprotoport=tcp
rightprotoport=tcp/7471
"""), "vxlan": Template("""\
conn $ifname-in-$version
$auth_section
leftprotoport=udp/4789
rightprotoport=udp
conn $ifname-out-$version
$auth_section
leftprotoport=udp
rightprotoport=udp/4789
""")}
vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
exiting = False
monitor = None
xfrm = None
class XFRM(object):
"""This class is a simple wrapper around ip-xfrm (8) command line
utility. We are using this class only for informational purposes
so that ovs-monitor-ipsec could verify that IKE keying daemon has
installed IPsec policies and security associations into kernel as
expected."""
def __init__(self, ip_root_prefix):
self.IP = ip_root_prefix + "/sbin/ip"
def get_policies(self):
"""This function returns IPsec policies (from kernel) in a dictionary
where <key> is destination IPv4 address and <value> is SELECTOR of
the IPsec policy."""
policies = {}
proc = subprocess.Popen([self.IP, 'xfrm', 'policy'],
stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline().strip().decode()
if line == '':
break
a = line.split(" ")
if len(a) >= 4 and a[0] == "src" and a[2] == "dst":
dst = (a[3].split("/"))[0]
if dst not in policies:
policies[dst] = []
policies[dst].append(line)
src = (a[3].split("/"))[0]
if src not in policies:
policies[src] = []
policies[src].append(line)
return policies
def get_securities(self):
"""This function returns IPsec security associations (from kernel)
in a dictionary where <key> is destination IPv4 address and <value>
is SELECTOR."""
securities = {}
proc = subprocess.Popen([self.IP, 'xfrm', 'state'],
stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline().strip().decode()
if line == '':
break
a = line.split(" ")
if len(a) >= 4 and a[0] == "sel" \
and a[1] == "src" and a[3] == "dst":
remote_ip = a[4].rstrip().split("/")[0]
local_ip = a[2].rstrip().split("/")[0]
if remote_ip not in securities:
securities[remote_ip] = []
securities[remote_ip].append(line)
if local_ip not in securities:
securities[local_ip] = []
securities[local_ip].append(line)
return securities
class StrongSwanHelper(object):
"""This class does StrongSwan specific configurations."""
STRONGSWAN_CONF = """%s
charon {
plugins {
kernel-netlink {
set_proto_port_transport_sa = yes
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_stt
type=drop
leftprotoport=tcp/7471
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")
subprocess.call([self.IPSEC, "restart"])
def get_active_conns(self):
"""This function parses output from 'ipsec status' command.
It returns dictionary where <key> is interface name (as in OVSDB)
and <value> is another dictionary. This another dictionary
uses strongSwan connection name as <key> and more detailed
sample line from the parsed outpus as <value>. """
conns = {}
proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline().strip().decode()
if line == '':
break
tunnel_name = line.split(":")
if len(tunnel_name) < 2:
continue
m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+).*", tunnel_name[0])
if not m:
continue
ifname = m.group(1)
if ifname not in conns:
conns[ifname] = {}
(conns[ifname])[tunnel_name[0]] = line
return conns
def config_init(self):
self.conf_file = open(self.IPSEC_CONF, "w")
self.secrets_file = open(self.IPSEC_SECRETS, "w")
self.conf_file.write(self.CONF_HEADER)
self.secrets_file.write(FILE_HEADER)
def config_global(self, monitor):
"""Configure the global state of IPsec tunnels."""
needs_refresh = False
if monitor.conf_in_use != monitor.conf:
monitor.conf_in_use = copy.deepcopy(monitor.conf)
needs_refresh = True
# Configure the shunt policy
if monitor.conf_in_use["skb_mark"]:
skb_mark = monitor.conf_in_use["skb_mark"]
self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
# Configure the CA cert
if monitor.conf_in_use["pki"]["ca_cert"]:
cacert = monitor.conf_in_use["pki"]["ca_cert"]
self.conf_file.write(self.CA_SECTION % cacert)
return needs_refresh
def config_tunnel(self, tunnel):
if tunnel.conf["psk"]:
self.secrets_file.write('%%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")
proc = subprocess.Popen([self.IPSEC, "update"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
outs, errs = proc.communicate()
if proc.returncode != 0:
vlog.err("StrongSwan failed to update configuration:\n"
"%s \n %s" % (str(outs), str(errs)))
subprocess.call([self.IPSEC, "rereadsecrets"])
# "ipsec update" command does not remove those tunnels that were
# updated or that disappeared from the ipsec.conf file. So, we have
# to manually remove them by calling "ipsec stroke down-nb <tunnel>"
# command. We use <version> number to tell apart tunnels that
# were just updated.
# "ipsec down-nb" command is designed to be non-blocking (opposed
# to "ipsec down" command). This means that we should not be concerned
# about possibility of ovs-monitor-ipsec to block for each tunnel
# while strongSwan sends IKE messages over Internet.
conns_dict = self.get_active_conns()
for ifname, conns in conns_dict.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))
subprocess.call([self.IPSEC, "stroke", "down-nb", conn])
class LibreSwanHelper(object):
"""This class does LibreSwan specific configurations."""
CONF_HEADER = """%s
config setup
uniqueids=yes
conn %%default
keyingtries=%%forever
type=transport
auto=route
ike=aes_gcm256-sha2_256
esp=aes_gcm256
ikev2=insist
""" % (FILE_HEADER)
SHUNT_POLICY = """conn prevent_unencrypted_gre
type=drop
left=%defaultroute
leftprotoport=gre
mark={0}
conn prevent_unencrypted_geneve
type=drop
left=%defaultroute
leftprotoport=udp/6081
mark={0}
conn prevent_unencrypted_stt
type=drop
left=%defaultroute
leftprotoport=tcp/7471
mark={0}
conn prevent_unencrypted_vxlan
type=drop
left=%defaultroute
leftprotoport=udp/4789
mark={0}
"""
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):
ipsec_conf = args.ipsec_conf if args.ipsec_conf else "/etc/ipsec.conf"
ipsec_d = args.ipsec_d if args.ipsec_d else "/etc/ipsec.d"
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 = libreswan_root_prefix + "/usr/sbin/ipsec"
self.IPSEC_CONF = libreswan_root_prefix + 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.secrets_file = None
vlog.dbg("Using: " + self.IPSEC)
vlog.dbg("Configuration file: " + self.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(self.CONF_HEADER)
f.close()
f = open(self.IPSEC_SECRETS, "w")
f.write(FILE_HEADER)
f.close()
vlog.info("Restarting LibreSwan")
subprocess.call([self.IPSEC, "restart"])
def config_init(self):
self.conf_file = open(self.IPSEC_CONF, "w")
self.secrets_file = open(self.IPSEC_SECRETS, "w")
self.conf_file.write(self.CONF_HEADER)
self.secrets_file.write(FILE_HEADER)
def config_global(self, monitor):
"""Configure the global state of IPsec tunnels."""
needs_refresh = False
if monitor.conf_in_use["pki"] != monitor.conf["pki"]:
# Clear old state
if monitor.conf_in_use["pki"]["certificate"]:
local_name = monitor.conf_in_use["pki"]["local_name"]
self._nss_delete_cert_and_key(self.CERTKEY_PREFIX + local_name)
if monitor.conf_in_use["pki"]["ca_cert"]:
self._nss_delete_cert(self.CERT_PREFIX + "cacert")
# Load new state
if monitor.conf["pki"]["certificate"]:
cert = monitor.conf["pki"]["certificate"]
key = monitor.conf["pki"]["private_key"]
name = monitor.conf["pki"]["local_name"]
name = self.CERTKEY_PREFIX + name
self._nss_import_cert_and_key(cert, key, name)
if monitor.conf["pki"]["ca_cert"]:
self._nss_import_cert(monitor.conf["pki"]["ca_cert"],
self.CERT_PREFIX + "cacert", 'CT,,')
monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"])
needs_refresh = True
# Configure the shunt policy
if monitor.conf["skb_mark"]:
skb_mark = monitor.conf["skb_mark"]
self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
# Will update conf_in_use later in the 'refresh' method
if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
needs_refresh = True
return needs_refresh
def config_tunnel(self, tunnel):
if tunnel.conf["psk"]:
self.secrets_file.write('%%any %s : PSK "%s"\n' %
(tunnel.conf["remote_ip"], tunnel.conf["psk"]))
auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
elif tunnel.conf["remote_cert"]:
auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)
self._nss_import_cert(tunnel.conf["remote_cert"],
self.CERT_PREFIX + tunnel.conf["remote_name"],
'P,P,P')
else:
auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf)
if tunnel.conf["address_family"] == "IPv6":
auth_section = self.IPV6_CONN + 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")
subprocess.call([self.IPSEC, "auto", "--ctlsocket", self.IPSEC_CTL,
"--config", self.IPSEC_CONF, "--rereadsecrets"])
tunnels = set(monitor.tunnels.keys())
# Delete old connections
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 must start with Interface name
if not conn.startswith(ifname):
vlog.err("%s does not start with %s" % (conn, ifname))
continue
# version number should be the first integer after
# interface name in IPsec "connection"
try:
ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
except ValueError:
vlog.err("%s does not contain version number")
continue
except IndexError:
vlog.err("%s does not contain version number")
continue
if not tunnel or tunnel.version != ver:
vlog.info("%s is outdated %u" % (conn, ver))
subprocess.call([self.IPSEC, "auto", "--ctlsocket",
self.IPSEC_CTL, "--config",
self.IPSEC_CONF, "--delete", conn])
elif ifname in tunnels:
tunnels.remove(ifname)
# Activate new connections
for name in tunnels:
ver = monitor.tunnels[name].version
if monitor.tunnels[name].conf["tunnel_type"] == "gre":
conn = "%s-%s" % (name, ver)
self._start_ipsec_connection(conn)
else:
conn_in = "%s-in-%s" % (name, ver)
conn_out = "%s-out-%s" % (name, ver)
self._start_ipsec_connection(conn_in)
self._start_ipsec_connection(conn_out)
# Update shunt policy if changed
if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
if monitor.conf["skb_mark"]:
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--add",
"--asynchronous", "prevent_unencrypted_gre"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--add",
"--asynchronous", "prevent_unencrypted_geneve"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--add",
"--asynchronous", "prevent_unencrypted_stt"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--add",
"--asynchronous", "prevent_unencrypted_vxlan"])
else:
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--delete",
"--asynchronous", "prevent_unencrypted_gre"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--delete",
"--asynchronous", "prevent_unencrypted_geneve"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--delete",
"--asynchronous", "prevent_unencrypted_stt"])
subprocess.call([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--delete",
"--asynchronous", "prevent_unencrypted_vxlan"])
monitor.conf_in_use["skb_mark"] = monitor.conf["skb_mark"]
def get_active_conns(self):
"""This function parses output from 'ipsec status' command.
It returns dictionary where <key> is interface name (as in OVSDB)
and <value> is another dictionary. This another dictionary
uses LibreSwan connection name as <key> and more detailed
sample line from the parsed outpus as <value>. """
conns = {}
proc = subprocess.Popen([self.IPSEC, 'status', '--ctlsocket',
self.IPSEC_CTL], stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline().strip().decode()
if line == '':
break
m = re.search(r"#\d+: \"(.*)\".*", line)
if not m:
continue
conn = m.group(1)
m = re.match(r"(.*)(-in-\d+|-out-\d+)", conn)
if not m:
# 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 _start_ipsec_connection(self, conn):
# In a corner case, LibreSwan daemon restarts for some reason and
# the "ipsec auto --start" command is lost. Just retry to make sure
# the command is received by LibreSwan.
while True:
proc = subprocess.Popen([self.IPSEC, "auto",
"--config", self.IPSEC_CONF,
"--ctlsocket", self.IPSEC_CTL,
"--start",
"--asynchronous", conn],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
perr = str(proc.stderr.read())
pout = str(proc.stdout.read())
if not re.match(r".*Connection refused.*", perr) and \
not re.match(r".*need --listen.*", pout):
break
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(pout)
def _nss_clear_database(self):
"""Remove all OVS IPsec related state from the NSS database"""
try:
proc = subprocess.Popen(['certutil', '-L', '-d',
self.IPSEC_D],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
lines = proc.stdout.readlines()
for line in lines:
s = line.strip().split()
if len(s) < 1:
continue
name = s[0]
if name.startswith(self.CERT_PREFIX):
self._nss_delete_cert(name)
elif name.startswith(self.CERTKEY_PREFIX):
self._nss_delete_cert_and_key(name)
except Exception as e:
vlog.err("Failed to clear NSS database.\n" + str(e))
def _nss_import_cert(self, cert, name, cert_type):
"""Cert_type is 'CT,,' for the CA certificate and 'P,P,P' for the
normal certificate."""
try:
proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert,
'-d', self.IPSEC_D, '-n',
name, '-t', cert_type],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
except Exception as e:
vlog.err("Failed to import certificate into NSS.\n" + str(e))
def _nss_delete_cert(self, name):
try:
proc = subprocess.Popen(['certutil', '-D', '-d',
self.IPSEC_D, '-n', name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
except Exception as e:
vlog.err("Failed to delete certificate from NSS.\n" + str(e))
def _nss_import_cert_and_key(self, cert, key, name):
try:
# Avoid deleting other files
path = os.path.abspath('/tmp/%s.p12' % name)
if not path.startswith('/tmp/'):
raise Exception("Illegal certificate name!")
# Create p12 file from pem files
proc = subprocess.Popen(['openssl', 'pkcs12', '-export',
'-in', cert, '-inkey', key, '-out',
path, '-name', name, '-passout', 'pass:'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
# Load p12 file to the database
proc = subprocess.Popen(['pk12util', '-i', path, '-d',
self.IPSEC_D, '-W', ''],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
except Exception as e:
vlog.err("Import cert and key failed.\n" + str(e))
os.remove(path)
def _nss_delete_cert_and_key(self, name):
try:
# Delete certificate and private key
proc = subprocess.Popen(['certutil', '-F', '-d',
self.IPSEC_D, '-n', name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
except Exception as e:
vlog.err("Delete cert and key failed.\n" + str(e))
class IPsecTunnel(object):
"""This is the base class for IPsec tunnel."""
unixctl_config_tmpl = Template("""\
Tunnel Type: $tunnel_type
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", "stt"]
def is_ipsec_required(self, options_column):
"""Return True if tunnel needs to be encrypted. Otherwise,
returns False."""
return "psk" in options_column or \
"remote_name" in options_column or \
"remote_cert" in options_column
def add_tunnel(self, name, row):
"""Adds a new tunnel that monitor will provision with 'name'."""
vlog.info("Tunnel %s appeared in OVSDB" % (name))
self.tunnels[name] = IPsecTunnel(name, row)
def update_tunnel(self, name, row):
"""Updates configuration of already existing tunnel with 'name'."""
tunnel = self.tunnels[name]
if tunnel.update_conf(row):
vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" %
(tunnel.name, tunnel.version))
def del_tunnel(self, name):
"""Deletes tunnel by 'name'."""
vlog.info("Tunnel %s disappeared from OVSDB" % (name))
self.tunnels[name].mark_for_removal()
def update_conf(self, pki, skb_mark):
"""Update the global configuration for IPsec tunnels"""
self.conf["pki"]["certificate"] = pki[0]
self.conf["pki"]["private_key"] = pki[1]
self.conf["pki"]["ca_cert"] = pki[2]
self.conf["pki"]["local_name"] = pki[3]
# Update skb_mark used in IPsec policies.
self.conf["skb_mark"] = skb_mark
def read_ovsdb_open_vswitch_table(self, data):
"""This functions reads IPsec relevant configuration from Open_vSwitch
table."""
pki = [None, None, None, None]
skb_mark = None
is_valid = False
for row in data["Open_vSwitch"].rows.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:
self.ike_helper.refresh(self)
def _get_cn_from_cert(self, cert):
try:
proc = subprocess.Popen(['openssl', 'x509', '-noout', '-subject',
'-nameopt', 'RFC2253', '-in', cert],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.wait()
if proc.returncode:
raise Exception(proc.stderr.read())
m = re.search(r"CN=(.+?),", proc.stdout.readline().decode())
if not m:
raise Exception("No CN in the certificate subject.")
except Exception as e:
vlog.warn(str(e))
return None
return m.group(1)
def 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 (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).")
ovs.vlog.add_args(parser)
ovs.daemon.add_args(parser)
args = parser.parse_args()
ovs.vlog.handle_args(args)
ovs.daemon.handle_args(args)
global monitor
global xfrm
root_prefix = args.root_prefix if args.root_prefix else ""
xfrm = XFRM(root_prefix)
monitor = IPsecMonitor(root_prefix, args.ike_daemon,
not args.no_restart_ike_daemon, args)
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.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)
|