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