]> git.proxmox.com Git - mirror_ovs.git/blame - ipsec/ovs-monitor-ipsec.in
ovs-monitor-ipsec: Suppress "unknown %d argument" warning.
[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("""\
419 left=%defaultroute
420 right=$remote_ip
421 authby=secret"""),
422 "pki_remote": Template("""\
423 left=%defaultroute
424 right=$remote_ip
425 leftid=@$local_name
426 rightid=@$remote_name
427 leftcert="$local_name"
428 rightcert="$remote_name"
429 leftrsasigkey=%cert"""),
430 "pki_ca": Template("""\
431 left=%defaultroute
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)
628 m = re.match(r"(.*)(-in-\d+|-out-\d+|-\d+)", conn)
629 if not m:
630 continue
631
632 ifname = m.group(1)
633 if ifname not in conns:
634 conns[ifname] = {}
635 (conns[ifname])[conn] = line
636
637 return conns
638
639 def _start_ipsec_connection(self, conn):
640 # In a corner case, LibreSwan daemon restarts for some reason and
641 # the "ipsec auto --start" command is lost. Just retry to make sure
642 # the command is received by LibreSwan.
643 while True:
644 proc = subprocess.Popen([self.IPSEC, "auto", "--start",
645 "--asynchronous", conn],
646 stdout=subprocess.PIPE,
647 stderr=subprocess.PIPE)
648 perr = str(proc.stderr.read())
649 pout = str(proc.stdout.read())
650 if not re.match(r".*Connection refused.*", perr) and \
651 not re.match(r".*need --listen.*", pout):
652 break
653
654 def _nss_clear_database(self):
655 """Remove all OVS IPsec related state from the NSS database"""
656 try:
657 proc = subprocess.Popen(['certutil', '-L', '-d',
658 'sql:/etc/ipsec.d/'],
659 stdout=subprocess.PIPE,
cafe492d
MG
660 stderr=subprocess.PIPE,
661 universal_newlines=True)
22c5eafb
QX
662 lines = proc.stdout.readlines()
663
664 for line in lines:
665 s = line.strip().split()
666 if len(s) < 1:
667 continue
668 name = s[0]
669 if name.startswith(self.CERT_PREFIX):
670 self._nss_delete_cert(name)
671 elif name.startswith(self.CERTKEY_PREFIX):
672 self._nss_delete_cert_and_key(name)
673
674 except Exception as e:
675 vlog.err("Failed to clear NSS database.\n" + str(e))
676
677 def _nss_import_cert(self, cert, name, cert_type):
678 """Cert_type is 'CT,,' for the CA certificate and 'P,P,P' for the
679 normal certificate."""
680 try:
681 proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert,
682 '-d', 'sql:/etc/ipsec.d/', '-n',
683 name, '-t', cert_type],
684 stdout=subprocess.PIPE,
685 stderr=subprocess.PIPE)
686 proc.wait()
687 if proc.returncode:
688 raise Exception(proc.stderr.read())
689 except Exception as e:
690 vlog.err("Failed to import ceretificate into NSS.\n" + str(e))
691
692 def _nss_delete_cert(self, name):
693 try:
694 proc = subprocess.Popen(['certutil', '-D', '-d',
695 'sql:/etc/ipsec.d/', '-n', name],
696 stdout=subprocess.PIPE,
697 stderr=subprocess.PIPE)
698 proc.wait()
699 if proc.returncode:
700 raise Exception(proc.stderr.read())
701 except Exception as e:
702 vlog.err("Failed to delete ceretificate from NSS.\n" + str(e))
703
704 def _nss_import_cert_and_key(self, cert, key, name):
705 try:
706 # Avoid deleting other files
707 path = os.path.abspath('/tmp/%s.p12' % name)
708 if not path.startswith('/tmp/'):
709 raise Exception("Illegal certificate name!")
710
711 # Create p12 file from pem files
712 proc = subprocess.Popen(['openssl', 'pkcs12', '-export',
713 '-in', cert, '-inkey', key, '-out',
714 path, '-name', name, '-passout', 'pass:'],
715 stdout=subprocess.PIPE,
716 stderr=subprocess.PIPE)
717 proc.wait()
718 if proc.returncode:
719 raise Exception(proc.stderr.read())
720
721 # Load p12 file to the database
722 proc = subprocess.Popen(['pk12util', '-i', path, '-d',
723 'sql:/etc/ipsec.d/', '-W', ''],
724 stdout=subprocess.PIPE,
725 stderr=subprocess.PIPE)
726 proc.wait()
727 if proc.returncode:
728 raise Exception(proc.stderr.read())
729
730 except Exception as e:
731 vlog.err("Import cert and key failed.\n" + str(e))
732 os.remove(path)
733
734 def _nss_delete_cert_and_key(self, name):
735 try:
736 # Delete certificate and private key
737 proc = subprocess.Popen(['certutil', '-F', '-d',
738 'sql:/etc/ipsec.d/', '-n', name],
739 stdout=subprocess.PIPE,
740 stderr=subprocess.PIPE)
741 proc.wait()
742 if proc.returncode:
743 raise Exception(proc.stderr.read())
744
745 except Exception as e:
746 vlog.err("Delete cert and key failed.\n" + str(e))
747
748
749class IPsecTunnel(object):
750 """This is the base class for IPsec tunnel."""
751
752 unixctl_config_tmpl = Template("""\
753 Tunnel Type: $tunnel_type
754 Remote IP: $remote_ip
755 SKB mark: $skb_mark
756 Local cert: $certificate
757 Local name: $local_name
758 Local key: $private_key
759 Remote cert: $remote_cert
760 Remote name: $remote_name
761 CA cert: $ca_cert
762 PSK: $psk
763""")
764
765 unixctl_status_tmpl = Template("""\
766 Ofport: $ofport
767 CFM state: $cfm_state
768""")
769
770 def __init__(self, name, row):
771 self.name = name # 'name' will not change because it is key in OVSDB
772 self.version = 0 # 'version' is increased on configuration changes
773 self.last_refreshed_version = -1
774 self.state = "INIT"
775 self.conf = {}
776 self.status = {}
777 self.update_conf(row)
778
779 def update_conf(self, row):
780 """This function updates IPsec tunnel configuration by using 'row'
781 from OVSDB interface table. If configuration was actually changed
782 in OVSDB then this function returns True. Otherwise, it returns
783 False."""
784 ret = False
785 options = row.options
786 remote_cert = options.get("remote_cert")
787 remote_name = options.get("remote_name")
788 if remote_cert:
789 remote_name = monitor._get_cn_from_cert(remote_cert)
790
791 new_conf = {
792 "ifname": self.name,
793 "tunnel_type": row.type,
794 "remote_ip": options.get("remote_ip"),
795 "skb_mark": monitor.conf["skb_mark"],
796 "certificate": monitor.conf["pki"]["certificate"],
797 "private_key": monitor.conf["pki"]["private_key"],
798 "ca_cert": monitor.conf["pki"]["ca_cert"],
799 "remote_cert": remote_cert,
800 "remote_name": remote_name,
801 "local_name": monitor.conf["pki"]["local_name"],
802 "psk": options.get("psk")}
803
804 if self.conf != new_conf:
805 # Configuration was updated in OVSDB. Validate it and figure
806 # out what to do next with this IPsec tunnel. Also, increment
807 # version number of this IPsec tunnel so that we could tell
808 # apart old and new tunnels in "ipsec status" output.
809 self.version += 1
810 ret = True
811 self.conf = new_conf
812
813 if self._is_valid_tunnel_conf():
814 self.state = "CONFIGURED"
815 else:
816 vlog.warn("%s contains invalid configuration%s" %
817 (self.name, self.invalid_reason))
818 self.state = "INVALID"
819
820 new_status = {
821 "cfm_state": "Up" if row.cfm_fault == [False] else
822 "Down" if row.cfm_fault == [True] else
823 "Disabled",
824 "ofport": "Not assigned" if (row.ofport in [[], [-1]]) else
825 row.ofport[0]}
826
827 if self.status != new_status:
828 # Tunnel has become unhealthy or ofport changed. Simply log this.
829 vlog.dbg("%s changed status from %s to %s" %
830 (self.name, str(self.status), str(new_status)))
831 self.status = new_status
832 return ret
833
834 def mark_for_removal(self):
835 """This function marks tunnel for removal."""
836 self.version += 1
837 self.state = "REMOVED"
838
839 def show(self, policies, securities, conns):
840 state = self.state
841 if self.state == "INVALID":
842 state += self.invalid_reason
843 header = "Interface name: %s v%u (%s)\n" % (self.name, self.version,
844 state)
845 conf = self.unixctl_config_tmpl.substitute(self.conf)
846 status = self.unixctl_status_tmpl.substitute(self.status)
847 spds = "Kernel policies installed:\n"
848 remote_ip = self.conf["remote_ip"]
849 if remote_ip in policies:
850 for line in policies[remote_ip]:
851 spds += " " + line + "\n"
852 sas = "Kernel security associations installed:\n"
853 if remote_ip in securities:
854 for line in securities[remote_ip]:
855 sas += " " + line + "\n"
856 cons = "IPsec connections that are active:\n"
857 if self.name in conns:
858 for tname in conns[self.name]:
859 cons += " " + conns[self.name][tname] + "\n"
860
861 return header + conf + status + spds + sas + cons + "\n"
862
863 def _is_valid_tunnel_conf(self):
864 """This function verifies if IPsec tunnel has valid configuration
865 set in 'conf'. If it is valid, then it returns True. Otherwise,
866 it returns False and sets the reason why configuration was considered
867 as invalid.
868
869 This function could be improved in future to also verify validness
870 of certificates themselves so that ovs-monitor-ipsec would not
871 pass malformed configuration to IKE daemon."""
872
873 self.invalid_reason = None
874
875 if not self.conf["remote_ip"]:
876 self.invalid_reason = ": 'remote_ip' is not set"
877 return False
878
879 if self.conf["psk"]:
880 if self.conf["certificate"] or self.conf["private_key"] \
881 or self.conf["ca_cert"] or self.conf["remote_cert"] \
882 or self.conf["remote_name"]:
883 self.invalid_reason = ": 'certificate', 'private_key', "\
884 "'ca_cert', 'remote_cert', and "\
885 "'remote_name' must be unset with PSK"
886 return False
887 # If configuring authentication with CA-signed certificate or
888 # self-signed certificate, the 'remote_name' should be specified at
889 # this point. When using CA-signed certificate, the 'remote_name' is
890 # read from interface's options field. When using self-signed
891 # certificate, the 'remote_name' is extracted from the 'remote_cert'
892 # file.
893 elif self.conf["remote_name"]:
894 if not self.conf["certificate"]:
895 self.invalid_reason = ": must set 'certificate' as local"\
896 " certificate when using CA-signed"\
897 " certificate or self-signed"\
898 " certificate to authenticate peers"
899 return False
900 elif not self.conf["private_key"]:
901 self.invalid_reason = ": must set 'private_key' as local"\
902 " private key when using CA-signed"\
903 " certificate or self-signed"\
904 " certificate to authenticate peers"
905 return False
906 if not self.conf["remote_cert"] and not self.conf["ca_cert"]:
907 self.invalid_reason = ": must set 'remote_cert' when using"\
908 " self-signed certificate"\
909 " authentication or 'ca_cert' when"\
910 " using CA-signed certificate"\
911 " authentication"
912 return False
913 else:
914 self.invalid_reason = ": must choose a authentication method"
915 return False
916
917 return True
918
919
920class IPsecMonitor(object):
921 """This class monitors and configures IPsec tunnels"""
922
923 def __init__(self, root_prefix, ike_daemon):
924 self.IPSEC = root_prefix + "/usr/sbin/ipsec"
925 self.tunnels = {}
926
927 # Global configuration shared by all tunnels
928 self.conf = {
929 "pki": {
930 "private_key": None,
931 "certificate": None,
932 "ca_cert": None,
933 "local_name": None
934 },
935 "skb_mark": None
936 }
937 self.conf_in_use = copy.deepcopy(self.conf)
938
939 # Choose to either use StrongSwan or LibreSwan as IKE daemon
940 if ike_daemon == "strongswan":
941 self.ike_helper = StrongSwanHelper(root_prefix)
942 elif ike_daemon == "libreswan":
943 self.ike_helper = LibreSwanHelper(root_prefix)
944 else:
945 vlog.err("The IKE daemon should be strongswan or libreswan.")
946 sys.exit(1)
947
948 # Check whether ipsec command is available
949 if not os.path.isfile(self.IPSEC) or \
950 not os.access(self.IPSEC, os.X_OK):
951 vlog.err("IKE daemon is not installed in the system.")
952
953 self.ike_helper.restart_ike_daemon()
954
955 def is_tunneling_type_supported(self, tunnel_type):
956 """Returns True if we know how to configure IPsec for these
957 types of tunnels. Otherwise, returns False."""
958 return tunnel_type in ["gre", "geneve", "vxlan", "stt"]
959
960 def is_ipsec_required(self, options_column):
961 """Return True if tunnel needs to be encrypted. Otherwise,
962 returns False."""
963 return "psk" in options_column or \
964 "remote_name" in options_column or \
965 "remote_cert" in options_column
966
967 def add_tunnel(self, name, row):
968 """Adds a new tunnel that monitor will provision with 'name'."""
969 vlog.info("Tunnel %s appeared in OVSDB" % (name))
970 self.tunnels[name] = IPsecTunnel(name, row)
971
972 def update_tunnel(self, name, row):
973 """Updates configuration of already existing tunnel with 'name'."""
974 tunnel = self.tunnels[name]
975 if tunnel.update_conf(row):
976 vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" %
977 (tunnel.name, tunnel.version))
978
979 def del_tunnel(self, name):
980 """Deletes tunnel by 'name'."""
981 vlog.info("Tunnel %s disappeared from OVSDB" % (name))
982 self.tunnels[name].mark_for_removal()
983
984 def update_conf(self, pki, skb_mark):
985 """Update the global configuration for IPsec tunnels"""
986 self.conf["pki"]["certificate"] = pki[0]
987 self.conf["pki"]["private_key"] = pki[1]
988 self.conf["pki"]["ca_cert"] = pki[2]
989 self.conf["pki"]["local_name"] = pki[3]
990
991 # Update skb_mark used in IPsec policies.
992 self.conf["skb_mark"] = skb_mark
993
994 def read_ovsdb_open_vswitch_table(self, data):
995 """This functions reads IPsec relevant configuration from Open_vSwitch
996 table."""
997 pki = [None, None, None, None]
998 skb_mark = None
999 is_valid = False
1000
8a09c259 1001 for row in data["Open_vSwitch"].rows.values():
22c5eafb
QX
1002 pki[0] = row.other_config.get("certificate")
1003 pki[1] = row.other_config.get("private_key")
1004 pki[2] = row.other_config.get("ca_cert")
1005 skb_mark = row.other_config.get("ipsec_skb_mark")
1006
1007 # Test whether it's a valid configration
1008 if pki[0] and pki[1]:
1009 pki[3] = self._get_cn_from_cert(pki[0])
1010 if pki[3]:
1011 is_valid = True
1012 elif not pki[0] and not pki[1] and not pki[2]:
1013 is_valid = True
1014
1015 if not is_valid:
1016 vlog.warn("The cert and key configuration is not valid. "
1017 "The valid configuations are 1): certificate, private_key "
1018 "and ca_cert are not set; or 2): certificate and "
1019 "private_key are all set.")
1020 else:
1021 self.update_conf(pki, skb_mark)
1022
1023 def read_ovsdb_interface_table(self, data):
1024 """This function reads the IPsec relevant configuration from Interface
1025 table."""
1026 ifaces = set()
1027
8a09c259 1028 for row in data["Interface"].rows.values():
22c5eafb
QX
1029 if not self.is_tunneling_type_supported(row.type):
1030 continue
1031 if not self.is_ipsec_required(row.options):
1032 continue
1033 if row.name in self.tunnels:
1034 self.update_tunnel(row.name, row)
1035 else:
1036 self.add_tunnel(row.name, row)
1037 ifaces.add(row.name)
1038
1039 # Mark for removal those tunnels that just disappeared from OVSDB
1040 for tunnel in self.tunnels.keys():
1041 if tunnel not in ifaces:
1042 self.del_tunnel(tunnel)
1043
1044 def read_ovsdb(self, data):
1045 """This function reads all configuration from OVSDB that
1046 ovs-monitor-ipsec is interested in."""
1047 self.read_ovsdb_open_vswitch_table(data)
1048 self.read_ovsdb_interface_table(data)
1049
1050 def show(self, unix_conn, policies, securities):
1051 """This function prints all tunnel state in 'unix_conn'.
1052 It uses 'policies' and securities' received from Linux Kernel
1053 to show if tunnels were actually configured by the IKE deamon."""
1054 if not self.tunnels:
1055 unix_conn.reply("No tunnels configured with IPsec")
1056 return
1057 s = ""
1058 conns = self.ike_helper.get_active_conns()
8a09c259 1059 for name, tunnel in self.tunnels.items():
22c5eafb
QX
1060 s += tunnel.show(policies, securities, conns)
1061 unix_conn.reply(s)
1062
1063 def run(self):
1064 """This function runs state machine that represents whole
1065 IPsec configuration (i.e. merged together from individual
1066 tunnel state machines). It creates configuration files and
1067 tells IKE daemon to update configuration."""
1068 needs_refresh = False
1069 removed_tunnels = []
1070
1071 self.ike_helper.config_init()
1072
1073 if self.ike_helper.config_global(self):
1074 needs_refresh = True
1075
8a09c259 1076 for name, tunnel in self.tunnels.items():
22c5eafb
QX
1077 if tunnel.last_refreshed_version != tunnel.version:
1078 tunnel.last_refreshed_version = tunnel.version
1079 needs_refresh = True
1080
1081 if tunnel.state == "REMOVED" or tunnel.state == "INVALID":
1082 removed_tunnels.append(name)
1083 elif tunnel.state == "CONFIGURED":
1084 self.ike_helper.config_tunnel(self.tunnels[name])
1085
1086 self.ike_helper.config_fini()
1087
1088 for name in removed_tunnels:
1089 # LibreSwan needs to clear state from database
1090 if hasattr(self.ike_helper, "clear_tunnel_state"):
1091 self.ike_helper.clear_tunnel_state(self.tunnels[name])
1092 del self.tunnels[name]
1093
1094 if needs_refresh:
1095 self.ike_helper.refresh(self)
1096
1097 def _get_cn_from_cert(self, cert):
1098 try:
1099 proc = subprocess.Popen(['openssl', 'x509', '-noout', '-subject',
1100 '-nameopt', 'RFC2253', '-in', cert],
1101 stdout=subprocess.PIPE,
1102 stderr=subprocess.PIPE)
1103 proc.wait()
1104 if proc.returncode:
1105 raise Exception(proc.stderr.read())
8a09c259 1106 m = re.search(r"CN=(.+?),", proc.stdout.readline().decode())
22c5eafb
QX
1107 if not m:
1108 raise Exception("No CN in the certificate subject.")
1109 except Exception as e:
1110 vlog.warn(str(e))
1111 return None
1112
1113 return m.group(1)
1114
1115
1116def unixctl_xfrm_policies(conn, unused_argv, unused_aux):
1117 global xfrm
1118 policies = xfrm.get_policies()
1119 conn.reply(str(policies))
1120
1121
1122def unixctl_xfrm_state(conn, unused_argv, unused_aux):
1123 global xfrm
1124 securities = xfrm.get_securities()
1125 conn.reply(str(securities))
1126
1127
1128def unixctl_ipsec_status(conn, unused_argv, unused_aux):
1129 global monitor
1130 conns = monitor.ike_helper.get_active_conns()
1131 conn.reply(str(conns))
1132
1133
1134def unixctl_show(conn, unused_argv, unused_aux):
1135 global monitor
1136 global xfrm
1137 policies = xfrm.get_policies()
1138 securities = xfrm.get_securities()
1139 monitor.show(conn, policies, securities)
1140
1141
1142def unixctl_refresh(conn, unused_argv, unused_aux):
1143 global monitor
1144 monitor.ike_helper.refresh(monitor)
1145 conn.reply(None)
1146
1147
1148def unixctl_exit(conn, unused_argv, unused_aux):
1149 global monitor
1150 global exiting
1151 exiting = True
1152
1153 # Make sure persistent global states are cleared
1154 monitor.update_conf([None, None, None, None], None)
1155 # Make sure persistent tunnel states are cleared
1156 for tunnel in monitor.tunnels.keys():
1157 monitor.del_tunnel(tunnel)
1158 monitor.run()
1159
1160 conn.reply(None)
1161
1162
1163def main():
1164 parser = argparse.ArgumentParser()
1165 parser.add_argument("database", metavar="DATABASE",
1166 help="A socket on which ovsdb-server is listening.")
1167 parser.add_argument("--root-prefix", metavar="DIR",
1168 help="Use DIR as alternate root directory"
1169 " (for testing).")
1170 parser.add_argument("--ike-daemon", metavar="IKE-DAEMON",
1171 help="The IKE daemon used for IPsec tunnels"
1172 " (either libreswan or strongswan).")
1173
1174 ovs.vlog.add_args(parser)
1175 ovs.daemon.add_args(parser)
1176 args = parser.parse_args()
1177 ovs.vlog.handle_args(args)
1178 ovs.daemon.handle_args(args)
1179
1180 global monitor
1181 global xfrm
1182
1183 root_prefix = args.root_prefix if args.root_prefix else ""
1184 xfrm = XFRM(root_prefix)
1185 monitor = IPsecMonitor(root_prefix, args.ike_daemon)
1186
1187 remote = args.database
1188 schema_helper = ovs.db.idl.SchemaHelper()
1189 schema_helper.register_columns("Interface",
1190 ["name", "type", "options", "cfm_fault",
1191 "ofport"])
1192 schema_helper.register_columns("Open_vSwitch", ["other_config"])
1193 idl = ovs.db.idl.Idl(remote, schema_helper)
1194
1195 ovs.daemon.daemonize()
1196
1197 ovs.unixctl.command_register("xfrm/policies", "", 0, 0,
1198 unixctl_xfrm_policies, None)
1199 ovs.unixctl.command_register("xfrm/state", "", 0, 0,
1200 unixctl_xfrm_state, None)
1201 ovs.unixctl.command_register("ipsec/status", "", 0, 0,
1202 unixctl_ipsec_status, None)
1203 ovs.unixctl.command_register("tunnels/show", "", 0, 0,
1204 unixctl_show, None)
1205 ovs.unixctl.command_register("refresh", "", 0, 0, unixctl_refresh, None)
1206 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
1207
1208 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
1209 if error:
1210 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
1211
1212 # Sequence number when OVSDB was processed last time
1213 seqno = idl.change_seqno
1214
1215 while True:
1216 unixctl_server.run()
1217 if exiting:
1218 break
1219
1220 idl.run()
1221 if seqno != idl.change_seqno:
1222 monitor.read_ovsdb(idl.tables)
1223 seqno = idl.change_seqno
1224
1225 monitor.run()
1226
1227 poller = ovs.poller.Poller()
1228 unixctl_server.wait(poller)
1229 idl.wait(poller)
1230 poller.block()
1231
1232 unixctl_server.close()
1233 idl.close()
1234
1235
1236if __name__ == '__main__':
1237 try:
1238 main()
1239 except SystemExit:
1240 # Let system.exit() calls complete normally
1241 raise
1242 except:
1243 vlog.exception("traceback")
1244 sys.exit(ovs.daemon.RESTART_EXIT_CODE)