]> git.proxmox.com Git - mirror_ovs.git/blame - ipsec/ovs-monitor-ipsec.in
cirrus: Use FreeBSD 12.2.
[mirror_ovs.git] / ipsec / ovs-monitor-ipsec.in
CommitLineData
1ca0323e 1#! @PYTHON3@
22c5eafb
QX
2# Copyright (c) 2017 Nicira, Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at:
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import argparse
17import re
18import subprocess
19import sys
20import copy
21import os
22from string import Template
23
24import ovs.daemon
25import ovs.db.idl
26import ovs.dirs
27import ovs.unixctl
28import ovs.unixctl.server
29import ovs.util
30import ovs.vlog
31
32
33FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n"
34transp_tmpl = {"gre": Template("""\
35conn $ifname-$version
36$auth_section
37 leftprotoport=gre
38 rightprotoport=gre
39
40"""), "gre64": Template("""\
41conn $ifname-$version
42$auth_section
43 leftprotoport=gre
44 rightprotoport=gre
45
46"""), "geneve": Template("""\
47conn $ifname-in-$version
48$auth_section
49 leftprotoport=udp/6081
50 rightprotoport=udp
51
52conn $ifname-out-$version
53$auth_section
54 leftprotoport=udp
55 rightprotoport=udp/6081
56
57"""), "stt": Template("""\
58conn $ifname-in-$version
59$auth_section
60 leftprotoport=tcp/7471
61 rightprotoport=tcp
62
63conn $ifname-out-$version
64$auth_section
65 leftprotoport=tcp
66 rightprotoport=tcp/7471
67
68"""), "vxlan": Template("""\
69conn $ifname-in-$version
70$auth_section
71 leftprotoport=udp/4789
72 rightprotoport=udp
73
74conn $ifname-out-$version
75$auth_section
76 leftprotoport=udp
77 rightprotoport=udp/4789
78
79""")}
80vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
81exiting = False
82monitor = None
83xfrm = None
84
85
86class XFRM(object):
87 """This class is a simple wrapper around ip-xfrm (8) command line
88 utility. We are using this class only for informational purposes
89 so that ovs-monitor-ipsec could verify that IKE keying daemon has
90 installed IPsec policies and security associations into kernel as
91 expected."""
92
93 def __init__(self, ip_root_prefix):
94 self.IP = ip_root_prefix + "/sbin/ip"
95
96 def get_policies(self):
97 """This function returns IPsec policies (from kernel) in a dictionary
98 where <key> is destination IPv4 address and <value> is SELECTOR of
99 the IPsec policy."""
100 policies = {}
101 proc = subprocess.Popen([self.IP, 'xfrm', 'policy'],
102 stdout=subprocess.PIPE)
103 while True:
8a09c259 104 line = proc.stdout.readline().strip().decode()
22c5eafb
QX
105 if line == '':
106 break
107 a = line.split(" ")
108 if len(a) >= 4 and a[0] == "src" and a[2] == "dst":
109 dst = (a[3].split("/"))[0]
110 if dst not in policies:
111 policies[dst] = []
112 policies[dst].append(line)
113 src = (a[3].split("/"))[0]
114 if src not in policies:
115 policies[src] = []
116 policies[src].append(line)
117 return policies
118
119 def get_securities(self):
120 """This function returns IPsec security associations (from kernel)
121 in a dictionary where <key> is destination IPv4 address and <value>
122 is SELECTOR."""
123 securities = {}
124 proc = subprocess.Popen([self.IP, 'xfrm', 'state'],
125 stdout=subprocess.PIPE)
126 while True:
8a09c259 127 line = proc.stdout.readline().strip().decode()
22c5eafb
QX
128 if line == '':
129 break
130 a = line.split(" ")
131 if len(a) >= 4 and a[0] == "sel" \
132 and a[1] == "src" and a[3] == "dst":
133 remote_ip = a[4].rstrip().split("/")[0]
134 local_ip = a[2].rstrip().split("/")[0]
135 if remote_ip not in securities:
136 securities[remote_ip] = []
137 securities[remote_ip].append(line)
138 if local_ip not in securities:
139 securities[local_ip] = []
140 securities[local_ip].append(line)
141 return securities
142
143
144class StrongSwanHelper(object):
145 """This class does StrongSwan specific configurations."""
146
147 STRONGSWAN_CONF = """%s
b424beca
BT
148charon {
149 plugins {
150 kernel-netlink {
151 set_proto_port_transport_sa = yes
152 xfrm_ack_expires = 10
153 }
154 gcm {
155 load = yes
156 }
157 }
158 load_modular = yes
159}
22c5eafb
QX
160""" % (FILE_HEADER)
161
162 CONF_HEADER = """%s
163config setup
164 uniqueids=yes
165
166conn %%default
167 keyingtries=%%forever
168 type=transport
169 keyexchange=ikev2
170 auto=route
171 ike=aes256gcm16-sha256-modp2048
172 esp=aes256gcm16-modp2048
173
174""" % (FILE_HEADER)
175
176 CA_SECTION = """ca ca_auth
177 cacert=%s
178
179"""
180
181 SHUNT_POLICY = """conn prevent_unencrypted_gre
182 type=drop
183 leftprotoport=gre
184 mark={0}
185
186conn prevent_unencrypted_geneve
187 type=drop
188 leftprotoport=udp/6081
189 mark={0}
190
191conn prevent_unencrypted_stt
192 type=drop
193 leftprotoport=tcp/7471
194 mark={0}
195
196conn prevent_unencrypted_vxlan
197 type=drop
198 leftprotoport=udp/4789
199 mark={0}
200
201"""
202
203 auth_tmpl = {"psk": Template("""\
204 left=0.0.0.0
205 right=$remote_ip
206 authby=psk"""),
207 "pki_remote": Template("""\
208 left=0.0.0.0
209 right=$remote_ip
210 leftid=$local_name
211 rightid=$remote_name
212 leftcert=$certificate
213 rightcert=$remote_cert"""),
214 "pki_ca": Template("""\
215 left=0.0.0.0
216 right=$remote_ip
217 leftid=$local_name
218 rightid=$remote_name
219 leftcert=$certificate""")}
220
221 def __init__(self, root_prefix):
222 self.CHARON_CONF = root_prefix + "/etc/strongswan.d/ovs.conf"
223 self.IPSEC = root_prefix + "/usr/sbin/ipsec"
224 self.IPSEC_CONF = root_prefix + "/etc/ipsec.conf"
225 self.IPSEC_SECRETS = root_prefix + "/etc/ipsec.secrets"
226 self.conf_file = None
227 self.secrets_file = None
228
229 def restart_ike_daemon(self):
230 """This function restarts StrongSwan."""
231 f = open(self.CHARON_CONF, "w")
232 f.write(self.STRONGSWAN_CONF)
233 f.close()
234
235 f = open(self.IPSEC_CONF, "w")
236 f.write(self.CONF_HEADER)
237 f.close()
238
239 f = open(self.IPSEC_SECRETS, "w")
240 f.write(FILE_HEADER)
241 f.close()
242
243 vlog.info("Restarting StrongSwan")
244 subprocess.call([self.IPSEC, "restart"])
245
246 def get_active_conns(self):
247 """This function parses output from 'ipsec status' command.
248 It returns dictionary where <key> is interface name (as in OVSDB)
249 and <value> is another dictionary. This another dictionary
250 uses strongSwan connection name as <key> and more detailed
251 sample line from the parsed outpus as <value>. """
252
253 conns = {}
254 proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE)
255
256 while True:
8a09c259 257 line = proc.stdout.readline().strip().decode()
22c5eafb
QX
258 if line == '':
259 break
260 tunnel_name = line.split(":")
261 if len(tunnel_name) < 2:
262 continue
263 m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+).*", tunnel_name[0])
264 if not m:
265 continue
266 ifname = m.group(1)
267 if ifname not in conns:
268 conns[ifname] = {}
269 (conns[ifname])[tunnel_name[0]] = line
270
271 return conns
272
273 def config_init(self):
274 self.conf_file = open(self.IPSEC_CONF, "w")
275 self.secrets_file = open(self.IPSEC_SECRETS, "w")
276 self.conf_file.write(self.CONF_HEADER)
277 self.secrets_file.write(FILE_HEADER)
278
279 def config_global(self, monitor):
280 """Configure the global state of IPsec tunnels."""
281 needs_refresh = False
282
283 if monitor.conf_in_use != monitor.conf:
284 monitor.conf_in_use = copy.deepcopy(monitor.conf)
285 needs_refresh = True
286
287 # Configure the shunt policy
288 if monitor.conf_in_use["skb_mark"]:
289 skb_mark = monitor.conf_in_use["skb_mark"]
290 self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
291
292 # Configure the CA cert
293 if monitor.conf_in_use["pki"]["ca_cert"]:
294 cacert = monitor.conf_in_use["pki"]["ca_cert"]
295 self.conf_file.write(self.CA_SECTION % cacert)
296
297 return needs_refresh
298
299 def config_tunnel(self, tunnel):
300 if tunnel.conf["psk"]:
301 self.secrets_file.write('0.0.0.0 %s : PSK "%s"\n' %
302 (tunnel.conf["remote_ip"], tunnel.conf["psk"]))
303 auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
304 else:
305 self.secrets_file.write("0.0.0.0 %s : RSA %s\n" %
306 (tunnel.conf["remote_ip"],
307 tunnel.conf["private_key"]))
308 if tunnel.conf["remote_cert"]:
309 tmpl = self.auth_tmpl["pki_remote"]
310 auth_section = tmpl.substitute(tunnel.conf)
311 else:
312 tmpl = self.auth_tmpl["pki_ca"]
313 auth_section = tmpl.substitute(tunnel.conf)
314
315 vals = tunnel.conf.copy()
316 vals["auth_section"] = auth_section
317 vals["version"] = tunnel.version
318 conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals)
319 self.conf_file.write(conf_text)
320
321 def config_fini(self):
322 self.secrets_file.close()
323 self.conf_file.close()
324 self.secrets_file = None
325 self.conf_file = None
326
327 def refresh(self, monitor):
328 """This functions refreshes strongSwan configuration. Behind the
329 scenes this function calls:
330 1. once "ipsec update" command that tells strongSwan to load
331 all new tunnels from "ipsec.conf"; and
332 2. once "ipsec rereadsecrets" command that tells strongswan to load
333 secrets from "ipsec.conf" file
334 3. for every removed tunnel "ipsec stroke down-nb <tunnel>" command
335 that removes old tunnels.
336 Once strongSwan vici bindings will be distributed with major
337 Linux distributions this function could be simplified."""
338 vlog.info("Refreshing StrongSwan configuration")
339 subprocess.call([self.IPSEC, "update"])
340 subprocess.call([self.IPSEC, "rereadsecrets"])
341 # "ipsec update" command does not remove those tunnels that were
342 # updated or that disappeared from the ipsec.conf file. So, we have
343 # to manually remove them by calling "ipsec stroke down-nb <tunnel>"
344 # command. We use <version> number to tell apart tunnels that
345 # were just updated.
346 # "ipsec down-nb" command is designed to be non-blocking (opposed
347 # to "ipsec down" command). This means that we should not be concerned
348 # about possibility of ovs-monitor-ipsec to block for each tunnel
349 # while strongSwan sends IKE messages over Internet.
350 conns_dict = self.get_active_conns()
8a09c259 351 for ifname, conns in conns_dict.items():
22c5eafb
QX
352 tunnel = monitor.tunnels.get(ifname)
353 for conn in conns:
354 # IPsec "connection" names that we choose in strongswan
355 # must start with Interface name
356 if not conn.startswith(ifname):
357 vlog.err("%s does not start with %s" % (conn, ifname))
358 continue
359
360 # version number should be the first integer after
361 # interface name in IPsec "connection"
362 try:
363 ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
364 except IndexError:
365 vlog.err("%s does not contain version number")
366 continue
367 except ValueError:
368 vlog.err("%s does not contain version number")
369 continue
370
371 if not tunnel or tunnel.version != ver:
372 vlog.info("%s is outdated %u" % (conn, ver))
373 subprocess.call([self.IPSEC, "stroke", "down-nb", conn])
374
375
376class LibreSwanHelper(object):
377 """This class does LibreSwan specific configurations."""
378 CONF_HEADER = """%s
379config setup
380 uniqueids=yes
381
382conn %%default
383 keyingtries=%%forever
384 type=transport
385 auto=route
386 ike=aes_gcm256-sha2_256
387 esp=aes_gcm256
388 ikev2=insist
389
390""" % (FILE_HEADER)
391
392 SHUNT_POLICY = """conn prevent_unencrypted_gre
393 type=drop
394 left=%defaultroute
395 leftprotoport=gre
396 mark={0}
397
398conn prevent_unencrypted_geneve
399 type=drop
400 left=%defaultroute
401 leftprotoport=udp/6081
402 mark={0}
403
404conn prevent_unencrypted_stt
405 type=drop
406 left=%defaultroute
407 leftprotoport=tcp/7471
408 mark={0}
409
410conn prevent_unencrypted_vxlan
411 type=drop
412 left=%defaultroute
413 leftprotoport=udp/4789
414 mark={0}
415
416"""
417
418 auth_tmpl = {"psk": Template("""\
1d4190c1 419 left=$local_ip
22c5eafb
QX
420 right=$remote_ip
421 authby=secret"""),
422 "pki_remote": Template("""\
1d4190c1 423 left=$local_ip
22c5eafb
QX
424 right=$remote_ip
425 leftid=@$local_name
426 rightid=@$remote_name
6d2a5be5
MG
427 leftcert="ovs_certkey_$local_name"
428 rightcert="ovs_cert_$remote_name"
22c5eafb
QX
429 leftrsasigkey=%cert"""),
430 "pki_ca": Template("""\
1d4190c1 431 left=$local_ip
22c5eafb
QX
432 right=$remote_ip
433 leftid=@$local_name
434 rightid=@$remote_name
435 leftcert="ovs_certkey_$local_name"
436 leftrsasigkey=%cert
437 rightca=%same""")}
438
439 CERT_PREFIX = "ovs_cert_"
440 CERTKEY_PREFIX = "ovs_certkey_"
441
442 def __init__(self, libreswan_root_prefix):
443 self.IPSEC = libreswan_root_prefix + "/usr/sbin/ipsec"
444 self.IPSEC_CONF = libreswan_root_prefix + "/etc/ipsec.conf"
445 self.IPSEC_SECRETS = libreswan_root_prefix + "/etc/ipsec.secrets"
446 self.conf_file = None
447 self.secrets_file = None
448
449 def restart_ike_daemon(self):
450 """This function restarts LibreSwan."""
451 # Remove the stale information from the NSS database
452 self._nss_clear_database()
453
454 f = open(self.IPSEC_CONF, "w")
455 f.write(self.CONF_HEADER)
456 f.close()
457
458 f = open(self.IPSEC_SECRETS, "w")
459 f.write(FILE_HEADER)
460 f.close()
461
462 vlog.info("Restarting LibreSwan")
463 subprocess.call([self.IPSEC, "restart"])
464
465 def config_init(self):
466 self.conf_file = open(self.IPSEC_CONF, "w")
467 self.secrets_file = open(self.IPSEC_SECRETS, "w")
468 self.conf_file.write(self.CONF_HEADER)
469 self.secrets_file.write(FILE_HEADER)
470
471 def config_global(self, monitor):
472 """Configure the global state of IPsec tunnels."""
473 needs_refresh = False
474
475 if monitor.conf_in_use["pki"] != monitor.conf["pki"]:
476 # Clear old state
477 if monitor.conf_in_use["pki"]["certificate"]:
478 local_name = monitor.conf_in_use["pki"]["local_name"]
479 self._nss_delete_cert_and_key(self.CERTKEY_PREFIX + local_name)
480
481 if monitor.conf_in_use["pki"]["ca_cert"]:
482 self._nss_delete_cert(self.CERT_PREFIX + "cacert")
483
484 # Load new state
485 if monitor.conf["pki"]["certificate"]:
486 cert = monitor.conf["pki"]["certificate"]
487 key = monitor.conf["pki"]["private_key"]
488 name = monitor.conf["pki"]["local_name"]
489 name = self.CERTKEY_PREFIX + name
490 self._nss_import_cert_and_key(cert, key, name)
491
492 if monitor.conf["pki"]["ca_cert"]:
493 self._nss_import_cert(monitor.conf["pki"]["ca_cert"],
494 self.CERT_PREFIX + "cacert", 'CT,,')
495
496 monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"])
497 needs_refresh = True
498
499 # Configure the shunt policy
500 if monitor.conf["skb_mark"]:
501 skb_mark = monitor.conf["skb_mark"]
502 self.conf_file.write(self.SHUNT_POLICY.format(skb_mark))
503
504 # Will update conf_in_use later in the 'refresh' method
505 if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
506 needs_refresh = True
507
508 return needs_refresh
509
510 def config_tunnel(self, tunnel):
511 if tunnel.conf["psk"]:
512 self.secrets_file.write('%%any %s : PSK "%s"\n' %
513 (tunnel.conf["remote_ip"], tunnel.conf["psk"]))
514 auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
515 elif tunnel.conf["remote_cert"]:
516 auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)
517 self._nss_import_cert(tunnel.conf["remote_cert"],
518 self.CERT_PREFIX + tunnel.conf["remote_name"],
519 'P,P,P')
520 else:
521 auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf)
522
523 vals = tunnel.conf.copy()
524 vals["auth_section"] = auth_section
525 vals["version"] = tunnel.version
526 conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals)
527 self.conf_file.write(conf_text)
528
529 def config_fini(self):
530 self.secrets_file.close()
531 self.conf_file.close()
532 self.secrets_file = None
533 self.conf_file = None
534
535 def clear_tunnel_state(self, tunnel):
536 if tunnel.conf["remote_cert"]:
537 name = self.CERT_PREFIX + tunnel.conf["remote_name"]
538 self._nss_delete_cert(name)
539
540 def refresh(self, monitor):
541 vlog.info("Refreshing LibreSwan configuration")
542 subprocess.call([self.IPSEC, "auto", "--rereadsecrets"])
543 tunnels = set(monitor.tunnels.keys())
544
545 # Delete old connections
546 conns_dict = self.get_active_conns()
8a09c259 547 for ifname, conns in conns_dict.items():
22c5eafb
QX
548 tunnel = monitor.tunnels.get(ifname)
549
550 for conn in conns:
551 # IPsec "connection" names must start with Interface name
552 if not conn.startswith(ifname):
553 vlog.err("%s does not start with %s" % (conn, ifname))
554 continue
555
556 # version number should be the first integer after
557 # interface name in IPsec "connection"
558 try:
559 ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
560 except ValueError:
561 vlog.err("%s does not contain version number")
562 continue
563 except IndexError:
564 vlog.err("%s does not contain version number")
565 continue
566
567 if not tunnel or tunnel.version != ver:
568 vlog.info("%s is outdated %u" % (conn, ver))
569 subprocess.call([self.IPSEC, "auto", "--delete", conn])
570 elif ifname in tunnels:
571 tunnels.remove(ifname)
572
573 # Activate new connections
574 for name in tunnels:
575 ver = monitor.tunnels[name].version
576
577 if monitor.tunnels[name].conf["tunnel_type"] == "gre":
578 conn = "%s-%s" % (name, ver)
579 self._start_ipsec_connection(conn)
580 else:
581 conn_in = "%s-in-%s" % (name, ver)
582 conn_out = "%s-out-%s" % (name, ver)
583 self._start_ipsec_connection(conn_in)
584 self._start_ipsec_connection(conn_out)
585
586 # Update shunt policy if changed
587 if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
588 if monitor.conf["skb_mark"]:
589 subprocess.call([self.IPSEC, "auto", "--add",
590 "--asynchronous", "prevent_unencrypted_gre"])
591 subprocess.call([self.IPSEC, "auto", "--add",
592 "--asynchronous", "prevent_unencrypted_geneve"])
593 subprocess.call([self.IPSEC, "auto", "--add",
594 "--asynchronous", "prevent_unencrypted_stt"])
595 subprocess.call([self.IPSEC, "auto", "--add",
596 "--asynchronous", "prevent_unencrypted_vxlan"])
597 else:
598 subprocess.call([self.IPSEC, "auto", "--delete",
599 "--asynchronous", "prevent_unencrypted_gre"])
600 subprocess.call([self.IPSEC, "auto", "--delete",
601 "--asynchronous", "prevent_unencrypted_geneve"])
602 subprocess.call([self.IPSEC, "auto", "--delete",
603 "--asynchronous", "prevent_unencrypted_stt"])
604 subprocess.call([self.IPSEC, "auto", "--delete",
605 "--asynchronous", "prevent_unencrypted_vxlan"])
606 monitor.conf_in_use["skb_mark"] = monitor.conf["skb_mark"]
607
608 def get_active_conns(self):
609 """This function parses output from 'ipsec status' command.
610 It returns dictionary where <key> is interface name (as in OVSDB)
611 and <value> is another dictionary. This another dictionary
612 uses LibreSwan connection name as <key> and more detailed
613 sample line from the parsed outpus as <value>. """
614
615 conns = {}
616 proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE)
617
618 while True:
8a09c259 619 line = proc.stdout.readline().strip().decode()
22c5eafb
QX
620 if line == '':
621 break
622
623 m = re.search(r"#\d+: \"(.*)\".*", line)
624 if not m:
625 continue
626
627 conn = m.group(1)
2ee0f448
MG
628 m = re.match(r"(.*)(-in-\d+|-out-\d+)", conn)
629 if not m:
630 # GRE connections have format <iface>-<ver>
631 m = re.match(r"(.*)(-\d+)", conn)
22c5eafb
QX
632 if not m:
633 continue
634
635 ifname = m.group(1)
636 if ifname not in conns:
637 conns[ifname] = {}
638 (conns[ifname])[conn] = line
639
640 return conns
641
642 def _start_ipsec_connection(self, conn):
643 # In a corner case, LibreSwan daemon restarts for some reason and
644 # the "ipsec auto --start" command is lost. Just retry to make sure
645 # the command is received by LibreSwan.
646 while True:
647 proc = subprocess.Popen([self.IPSEC, "auto", "--start",
648 "--asynchronous", conn],
649 stdout=subprocess.PIPE,
650 stderr=subprocess.PIPE)
651 perr = str(proc.stderr.read())
652 pout = str(proc.stdout.read())
653 if not re.match(r".*Connection refused.*", perr) and \
654 not re.match(r".*need --listen.*", pout):
655 break
656
657 def _nss_clear_database(self):
658 """Remove all OVS IPsec related state from the NSS database"""
659 try:
660 proc = subprocess.Popen(['certutil', '-L', '-d',
661 'sql:/etc/ipsec.d/'],
662 stdout=subprocess.PIPE,
cafe492d
MG
663 stderr=subprocess.PIPE,
664 universal_newlines=True)
22c5eafb
QX
665 lines = proc.stdout.readlines()
666
667 for line in lines:
668 s = line.strip().split()
669 if len(s) < 1:
670 continue
671 name = s[0]
672 if name.startswith(self.CERT_PREFIX):
673 self._nss_delete_cert(name)
674 elif name.startswith(self.CERTKEY_PREFIX):
675 self._nss_delete_cert_and_key(name)
676
677 except Exception as e:
678 vlog.err("Failed to clear NSS database.\n" + str(e))
679
680 def _nss_import_cert(self, cert, name, cert_type):
681 """Cert_type is 'CT,,' for the CA certificate and 'P,P,P' for the
682 normal certificate."""
683 try:
684 proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert,
685 '-d', 'sql:/etc/ipsec.d/', '-n',
686 name, '-t', cert_type],
687 stdout=subprocess.PIPE,
688 stderr=subprocess.PIPE)
689 proc.wait()
690 if proc.returncode:
691 raise Exception(proc.stderr.read())
692 except Exception as e:
6d2a5be5 693 vlog.err("Failed to import certificate into NSS.\n" + str(e))
22c5eafb
QX
694
695 def _nss_delete_cert(self, name):
696 try:
697 proc = subprocess.Popen(['certutil', '-D', '-d',
698 'sql:/etc/ipsec.d/', '-n', name],
699 stdout=subprocess.PIPE,
700 stderr=subprocess.PIPE)
701 proc.wait()
702 if proc.returncode:
703 raise Exception(proc.stderr.read())
704 except Exception as e:
6d2a5be5 705 vlog.err("Failed to delete certificate from NSS.\n" + str(e))
22c5eafb
QX
706
707 def _nss_import_cert_and_key(self, cert, key, name):
708 try:
709 # Avoid deleting other files
710 path = os.path.abspath('/tmp/%s.p12' % name)
711 if not path.startswith('/tmp/'):
712 raise Exception("Illegal certificate name!")
713
714 # Create p12 file from pem files
715 proc = subprocess.Popen(['openssl', 'pkcs12', '-export',
716 '-in', cert, '-inkey', key, '-out',
717 path, '-name', name, '-passout', 'pass:'],
718 stdout=subprocess.PIPE,
719 stderr=subprocess.PIPE)
720 proc.wait()
721 if proc.returncode:
722 raise Exception(proc.stderr.read())
723
724 # Load p12 file to the database
725 proc = subprocess.Popen(['pk12util', '-i', path, '-d',
726 'sql:/etc/ipsec.d/', '-W', ''],
727 stdout=subprocess.PIPE,
728 stderr=subprocess.PIPE)
729 proc.wait()
730 if proc.returncode:
731 raise Exception(proc.stderr.read())
732
733 except Exception as e:
734 vlog.err("Import cert and key failed.\n" + str(e))
735 os.remove(path)
736
737 def _nss_delete_cert_and_key(self, name):
738 try:
739 # Delete certificate and private key
740 proc = subprocess.Popen(['certutil', '-F', '-d',
741 'sql:/etc/ipsec.d/', '-n', name],
742 stdout=subprocess.PIPE,
743 stderr=subprocess.PIPE)
744 proc.wait()
745 if proc.returncode:
746 raise Exception(proc.stderr.read())
747
748 except Exception as e:
749 vlog.err("Delete cert and key failed.\n" + str(e))
750
751
752class IPsecTunnel(object):
753 """This is the base class for IPsec tunnel."""
754
755 unixctl_config_tmpl = Template("""\
756 Tunnel Type: $tunnel_type
1d4190c1 757 Local IP: $local_ip
22c5eafb
QX
758 Remote IP: $remote_ip
759 SKB mark: $skb_mark
760 Local cert: $certificate
761 Local name: $local_name
762 Local key: $private_key
763 Remote cert: $remote_cert
764 Remote name: $remote_name
765 CA cert: $ca_cert
766 PSK: $psk
767""")
768
769 unixctl_status_tmpl = Template("""\
770 Ofport: $ofport
771 CFM state: $cfm_state
772""")
773
774 def __init__(self, name, row):
775 self.name = name # 'name' will not change because it is key in OVSDB
776 self.version = 0 # 'version' is increased on configuration changes
777 self.last_refreshed_version = -1
778 self.state = "INIT"
779 self.conf = {}
780 self.status = {}
781 self.update_conf(row)
782
783 def update_conf(self, row):
784 """This function updates IPsec tunnel configuration by using 'row'
785 from OVSDB interface table. If configuration was actually changed
786 in OVSDB then this function returns True. Otherwise, it returns
787 False."""
788 ret = False
789 options = row.options
790 remote_cert = options.get("remote_cert")
791 remote_name = options.get("remote_name")
792 if remote_cert:
793 remote_name = monitor._get_cn_from_cert(remote_cert)
794
795 new_conf = {
796 "ifname": self.name,
797 "tunnel_type": row.type,
1d4190c1 798 "local_ip": options.get("local_ip", "%defaultroute"),
22c5eafb
QX
799 "remote_ip": options.get("remote_ip"),
800 "skb_mark": monitor.conf["skb_mark"],
801 "certificate": monitor.conf["pki"]["certificate"],
802 "private_key": monitor.conf["pki"]["private_key"],
803 "ca_cert": monitor.conf["pki"]["ca_cert"],
804 "remote_cert": remote_cert,
805 "remote_name": remote_name,
806 "local_name": monitor.conf["pki"]["local_name"],
807 "psk": options.get("psk")}
808
809 if self.conf != new_conf:
810 # Configuration was updated in OVSDB. Validate it and figure
811 # out what to do next with this IPsec tunnel. Also, increment
812 # version number of this IPsec tunnel so that we could tell
813 # apart old and new tunnels in "ipsec status" output.
814 self.version += 1
815 ret = True
816 self.conf = new_conf
817
818 if self._is_valid_tunnel_conf():
819 self.state = "CONFIGURED"
820 else:
821 vlog.warn("%s contains invalid configuration%s" %
822 (self.name, self.invalid_reason))
823 self.state = "INVALID"
824
825 new_status = {
826 "cfm_state": "Up" if row.cfm_fault == [False] else
827 "Down" if row.cfm_fault == [True] else
828 "Disabled",
829 "ofport": "Not assigned" if (row.ofport in [[], [-1]]) else
830 row.ofport[0]}
831
832 if self.status != new_status:
833 # Tunnel has become unhealthy or ofport changed. Simply log this.
834 vlog.dbg("%s changed status from %s to %s" %
835 (self.name, str(self.status), str(new_status)))
836 self.status = new_status
837 return ret
838
839 def mark_for_removal(self):
840 """This function marks tunnel for removal."""
841 self.version += 1
842 self.state = "REMOVED"
843
844 def show(self, policies, securities, conns):
845 state = self.state
846 if self.state == "INVALID":
847 state += self.invalid_reason
848 header = "Interface name: %s v%u (%s)\n" % (self.name, self.version,
849 state)
850 conf = self.unixctl_config_tmpl.substitute(self.conf)
851 status = self.unixctl_status_tmpl.substitute(self.status)
852 spds = "Kernel policies installed:\n"
853 remote_ip = self.conf["remote_ip"]
854 if remote_ip in policies:
855 for line in policies[remote_ip]:
856 spds += " " + line + "\n"
857 sas = "Kernel security associations installed:\n"
858 if remote_ip in securities:
859 for line in securities[remote_ip]:
860 sas += " " + line + "\n"
861 cons = "IPsec connections that are active:\n"
862 if self.name in conns:
863 for tname in conns[self.name]:
864 cons += " " + conns[self.name][tname] + "\n"
865
866 return header + conf + status + spds + sas + cons + "\n"
867
868 def _is_valid_tunnel_conf(self):
869 """This function verifies if IPsec tunnel has valid configuration
870 set in 'conf'. If it is valid, then it returns True. Otherwise,
871 it returns False and sets the reason why configuration was considered
872 as invalid.
873
874 This function could be improved in future to also verify validness
875 of certificates themselves so that ovs-monitor-ipsec would not
876 pass malformed configuration to IKE daemon."""
877
878 self.invalid_reason = None
879
880 if not self.conf["remote_ip"]:
881 self.invalid_reason = ": 'remote_ip' is not set"
882 return False
883
884 if self.conf["psk"]:
885 if self.conf["certificate"] or self.conf["private_key"] \
886 or self.conf["ca_cert"] or self.conf["remote_cert"] \
887 or self.conf["remote_name"]:
888 self.invalid_reason = ": 'certificate', 'private_key', "\
889 "'ca_cert', 'remote_cert', and "\
890 "'remote_name' must be unset with PSK"
891 return False
892 # If configuring authentication with CA-signed certificate or
893 # self-signed certificate, the 'remote_name' should be specified at
894 # this point. When using CA-signed certificate, the 'remote_name' is
895 # read from interface's options field. When using self-signed
896 # certificate, the 'remote_name' is extracted from the 'remote_cert'
897 # file.
898 elif self.conf["remote_name"]:
899 if not self.conf["certificate"]:
900 self.invalid_reason = ": must set 'certificate' as local"\
901 " certificate when using CA-signed"\
902 " certificate or self-signed"\
903 " certificate to authenticate peers"
904 return False
905 elif not self.conf["private_key"]:
906 self.invalid_reason = ": must set 'private_key' as local"\
907 " private key when using CA-signed"\
908 " certificate or self-signed"\
909 " certificate to authenticate peers"
910 return False
911 if not self.conf["remote_cert"] and not self.conf["ca_cert"]:
912 self.invalid_reason = ": must set 'remote_cert' when using"\
913 " self-signed certificate"\
914 " authentication or 'ca_cert' when"\
915 " using CA-signed certificate"\
916 " authentication"
917 return False
918 else:
919 self.invalid_reason = ": must choose a authentication method"
920 return False
921
922 return True
923
924
925class IPsecMonitor(object):
926 """This class monitors and configures IPsec tunnels"""
927
fe5ff26a 928 def __init__(self, root_prefix, ike_daemon, restart):
22c5eafb
QX
929 self.IPSEC = root_prefix + "/usr/sbin/ipsec"
930 self.tunnels = {}
931
932 # Global configuration shared by all tunnels
933 self.conf = {
934 "pki": {
935 "private_key": None,
936 "certificate": None,
937 "ca_cert": None,
938 "local_name": None
939 },
940 "skb_mark": None
941 }
942 self.conf_in_use = copy.deepcopy(self.conf)
943
944 # Choose to either use StrongSwan or LibreSwan as IKE daemon
945 if ike_daemon == "strongswan":
946 self.ike_helper = StrongSwanHelper(root_prefix)
947 elif ike_daemon == "libreswan":
948 self.ike_helper = LibreSwanHelper(root_prefix)
949 else:
950 vlog.err("The IKE daemon should be strongswan or libreswan.")
951 sys.exit(1)
952
953 # Check whether ipsec command is available
954 if not os.path.isfile(self.IPSEC) or \
955 not os.access(self.IPSEC, os.X_OK):
956 vlog.err("IKE daemon is not installed in the system.")
957
fe5ff26a
MG
958 if restart:
959 vlog.info("Restarting IKE daemon")
960 self.ike_helper.restart_ike_daemon()
22c5eafb
QX
961
962 def is_tunneling_type_supported(self, tunnel_type):
963 """Returns True if we know how to configure IPsec for these
964 types of tunnels. Otherwise, returns False."""
965 return tunnel_type in ["gre", "geneve", "vxlan", "stt"]
966
967 def is_ipsec_required(self, options_column):
968 """Return True if tunnel needs to be encrypted. Otherwise,
969 returns False."""
970 return "psk" in options_column or \
971 "remote_name" in options_column or \
972 "remote_cert" in options_column
973
974 def add_tunnel(self, name, row):
975 """Adds a new tunnel that monitor will provision with 'name'."""
976 vlog.info("Tunnel %s appeared in OVSDB" % (name))
977 self.tunnels[name] = IPsecTunnel(name, row)
978
979 def update_tunnel(self, name, row):
980 """Updates configuration of already existing tunnel with 'name'."""
981 tunnel = self.tunnels[name]
982 if tunnel.update_conf(row):
983 vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" %
984 (tunnel.name, tunnel.version))
985
986 def del_tunnel(self, name):
987 """Deletes tunnel by 'name'."""
988 vlog.info("Tunnel %s disappeared from OVSDB" % (name))
989 self.tunnels[name].mark_for_removal()
990
991 def update_conf(self, pki, skb_mark):
992 """Update the global configuration for IPsec tunnels"""
993 self.conf["pki"]["certificate"] = pki[0]
994 self.conf["pki"]["private_key"] = pki[1]
995 self.conf["pki"]["ca_cert"] = pki[2]
996 self.conf["pki"]["local_name"] = pki[3]
997
998 # Update skb_mark used in IPsec policies.
999 self.conf["skb_mark"] = skb_mark
1000
1001 def read_ovsdb_open_vswitch_table(self, data):
1002 """This functions reads IPsec relevant configuration from Open_vSwitch
1003 table."""
1004 pki = [None, None, None, None]
1005 skb_mark = None
1006 is_valid = False
1007
8a09c259 1008 for row in data["Open_vSwitch"].rows.values():
22c5eafb
QX
1009 pki[0] = row.other_config.get("certificate")
1010 pki[1] = row.other_config.get("private_key")
1011 pki[2] = row.other_config.get("ca_cert")
1012 skb_mark = row.other_config.get("ipsec_skb_mark")
1013
1014 # Test whether it's a valid configration
1015 if pki[0] and pki[1]:
1016 pki[3] = self._get_cn_from_cert(pki[0])
1017 if pki[3]:
1018 is_valid = True
1019 elif not pki[0] and not pki[1] and not pki[2]:
1020 is_valid = True
1021
1022 if not is_valid:
1023 vlog.warn("The cert and key configuration is not valid. "
1024 "The valid configuations are 1): certificate, private_key "
1025 "and ca_cert are not set; or 2): certificate and "
1026 "private_key are all set.")
1027 else:
1028 self.update_conf(pki, skb_mark)
1029
1030 def read_ovsdb_interface_table(self, data):
1031 """This function reads the IPsec relevant configuration from Interface
1032 table."""
1033 ifaces = set()
1034
8a09c259 1035 for row in data["Interface"].rows.values():
22c5eafb
QX
1036 if not self.is_tunneling_type_supported(row.type):
1037 continue
1038 if not self.is_ipsec_required(row.options):
1039 continue
1040 if row.name in self.tunnels:
1041 self.update_tunnel(row.name, row)
1042 else:
1043 self.add_tunnel(row.name, row)
1044 ifaces.add(row.name)
1045
1046 # Mark for removal those tunnels that just disappeared from OVSDB
1047 for tunnel in self.tunnels.keys():
1048 if tunnel not in ifaces:
1049 self.del_tunnel(tunnel)
1050
1051 def read_ovsdb(self, data):
1052 """This function reads all configuration from OVSDB that
1053 ovs-monitor-ipsec is interested in."""
1054 self.read_ovsdb_open_vswitch_table(data)
1055 self.read_ovsdb_interface_table(data)
1056
1057 def show(self, unix_conn, policies, securities):
1058 """This function prints all tunnel state in 'unix_conn'.
1059 It uses 'policies' and securities' received from Linux Kernel
1060 to show if tunnels were actually configured by the IKE deamon."""
1061 if not self.tunnels:
1062 unix_conn.reply("No tunnels configured with IPsec")
1063 return
1064 s = ""
1065 conns = self.ike_helper.get_active_conns()
8a09c259 1066 for name, tunnel in self.tunnels.items():
22c5eafb
QX
1067 s += tunnel.show(policies, securities, conns)
1068 unix_conn.reply(s)
1069
1070 def run(self):
1071 """This function runs state machine that represents whole
1072 IPsec configuration (i.e. merged together from individual
1073 tunnel state machines). It creates configuration files and
1074 tells IKE daemon to update configuration."""
1075 needs_refresh = False
1076 removed_tunnels = []
1077
1078 self.ike_helper.config_init()
1079
1080 if self.ike_helper.config_global(self):
1081 needs_refresh = True
1082
8a09c259 1083 for name, tunnel in self.tunnels.items():
22c5eafb
QX
1084 if tunnel.last_refreshed_version != tunnel.version:
1085 tunnel.last_refreshed_version = tunnel.version
1086 needs_refresh = True
1087
1088 if tunnel.state == "REMOVED" or tunnel.state == "INVALID":
1089 removed_tunnels.append(name)
1090 elif tunnel.state == "CONFIGURED":
1091 self.ike_helper.config_tunnel(self.tunnels[name])
1092
1093 self.ike_helper.config_fini()
1094
1095 for name in removed_tunnels:
1096 # LibreSwan needs to clear state from database
1097 if hasattr(self.ike_helper, "clear_tunnel_state"):
1098 self.ike_helper.clear_tunnel_state(self.tunnels[name])
1099 del self.tunnels[name]
1100
1101 if needs_refresh:
1102 self.ike_helper.refresh(self)
1103
1104 def _get_cn_from_cert(self, cert):
1105 try:
1106 proc = subprocess.Popen(['openssl', 'x509', '-noout', '-subject',
1107 '-nameopt', 'RFC2253', '-in', cert],
1108 stdout=subprocess.PIPE,
1109 stderr=subprocess.PIPE)
1110 proc.wait()
1111 if proc.returncode:
1112 raise Exception(proc.stderr.read())
8a09c259 1113 m = re.search(r"CN=(.+?),", proc.stdout.readline().decode())
22c5eafb
QX
1114 if not m:
1115 raise Exception("No CN in the certificate subject.")
1116 except Exception as e:
1117 vlog.warn(str(e))
1118 return None
1119
1120 return m.group(1)
1121
1122
1123def unixctl_xfrm_policies(conn, unused_argv, unused_aux):
1124 global xfrm
1125 policies = xfrm.get_policies()
1126 conn.reply(str(policies))
1127
1128
1129def unixctl_xfrm_state(conn, unused_argv, unused_aux):
1130 global xfrm
1131 securities = xfrm.get_securities()
1132 conn.reply(str(securities))
1133
1134
1135def unixctl_ipsec_status(conn, unused_argv, unused_aux):
1136 global monitor
1137 conns = monitor.ike_helper.get_active_conns()
1138 conn.reply(str(conns))
1139
1140
1141def unixctl_show(conn, unused_argv, unused_aux):
1142 global monitor
1143 global xfrm
1144 policies = xfrm.get_policies()
1145 securities = xfrm.get_securities()
1146 monitor.show(conn, policies, securities)
1147
1148
1149def unixctl_refresh(conn, unused_argv, unused_aux):
1150 global monitor
1151 monitor.ike_helper.refresh(monitor)
1152 conn.reply(None)
1153
1154
aa8bed09 1155def unixctl_exit(conn, argv, unused_aux):
22c5eafb
QX
1156 global monitor
1157 global exiting
aa8bed09 1158 ret = None
22c5eafb 1159 exiting = True
aa8bed09 1160 cleanup = True
22c5eafb 1161
aa8bed09
MG
1162 for arg in argv:
1163 if arg == "--no-cleanup":
1164 cleanup = False
1165 else:
1166 cleanup = False
1167 exiting = False
1168 ret = str("unrecognized parameter: %s" % arg)
1169
1170 if cleanup:
1171 # Make sure persistent global states are cleared
1172 monitor.update_conf([None, None, None, None], None)
1173 # Make sure persistent tunnel states are cleared
1174 for tunnel in monitor.tunnels.keys():
1175 monitor.del_tunnel(tunnel)
1176 monitor.run()
22c5eafb 1177
aa8bed09 1178 conn.reply(ret)
22c5eafb
QX
1179
1180
1181def main():
1182 parser = argparse.ArgumentParser()
1183 parser.add_argument("database", metavar="DATABASE",
1184 help="A socket on which ovsdb-server is listening.")
1185 parser.add_argument("--root-prefix", metavar="DIR",
1186 help="Use DIR as alternate root directory"
1187 " (for testing).")
1188 parser.add_argument("--ike-daemon", metavar="IKE-DAEMON",
1189 help="The IKE daemon used for IPsec tunnels"
1190 " (either libreswan or strongswan).")
fe5ff26a
MG
1191 parser.add_argument("--no-restart-ike-daemon", action='store_true',
1192 help="Don't restart the IKE daemon on startup.")
22c5eafb
QX
1193
1194 ovs.vlog.add_args(parser)
1195 ovs.daemon.add_args(parser)
1196 args = parser.parse_args()
1197 ovs.vlog.handle_args(args)
1198 ovs.daemon.handle_args(args)
1199
1200 global monitor
1201 global xfrm
1202
1203 root_prefix = args.root_prefix if args.root_prefix else ""
1204 xfrm = XFRM(root_prefix)
fe5ff26a
MG
1205 monitor = IPsecMonitor(root_prefix, args.ike_daemon,
1206 not args.no_restart_ike_daemon)
22c5eafb
QX
1207
1208 remote = args.database
1209 schema_helper = ovs.db.idl.SchemaHelper()
1210 schema_helper.register_columns("Interface",
1211 ["name", "type", "options", "cfm_fault",
1212 "ofport"])
1213 schema_helper.register_columns("Open_vSwitch", ["other_config"])
1214 idl = ovs.db.idl.Idl(remote, schema_helper)
1215
1216 ovs.daemon.daemonize()
1217
1218 ovs.unixctl.command_register("xfrm/policies", "", 0, 0,
1219 unixctl_xfrm_policies, None)
1220 ovs.unixctl.command_register("xfrm/state", "", 0, 0,
1221 unixctl_xfrm_state, None)
1222 ovs.unixctl.command_register("ipsec/status", "", 0, 0,
1223 unixctl_ipsec_status, None)
1224 ovs.unixctl.command_register("tunnels/show", "", 0, 0,
1225 unixctl_show, None)
1226 ovs.unixctl.command_register("refresh", "", 0, 0, unixctl_refresh, None)
aa8bed09
MG
1227 ovs.unixctl.command_register("exit", "[--no-cleanup]", 0, 1,
1228 unixctl_exit, None)
22c5eafb
QX
1229
1230 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
1231 if error:
1232 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
1233
1234 # Sequence number when OVSDB was processed last time
1235 seqno = idl.change_seqno
1236
1237 while True:
1238 unixctl_server.run()
1239 if exiting:
1240 break
1241
1242 idl.run()
1243 if seqno != idl.change_seqno:
1244 monitor.read_ovsdb(idl.tables)
1245 seqno = idl.change_seqno
1246
1247 monitor.run()
1248
1249 poller = ovs.poller.Poller()
1250 unixctl_server.wait(poller)
1251 idl.wait(poller)
1252 poller.block()
1253
1254 unixctl_server.close()
1255 idl.close()
1256
1257
1258if __name__ == '__main__':
1259 try:
1260 main()
1261 except SystemExit:
1262 # Let system.exit() calls complete normally
1263 raise
1264 except:
1265 vlog.exception("traceback")
1266 sys.exit(ovs.daemon.RESTART_EXIT_CODE)