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