]>
Commit | Line | Data |
---|---|---|
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 | ||
16 | import argparse | |
17 | import re | |
18 | import subprocess | |
19 | import sys | |
20 | import copy | |
21 | import os | |
22 | from string import Template | |
23 | ||
24 | import ovs.daemon | |
25 | import ovs.db.idl | |
26 | import ovs.dirs | |
27 | import ovs.unixctl | |
28 | import ovs.unixctl.server | |
29 | import ovs.util | |
30 | import ovs.vlog | |
31 | ||
32 | ||
33 | FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n" | |
34 | transp_tmpl = {"gre": Template("""\ | |
35 | conn $ifname-$version | |
36 | $auth_section | |
37 | leftprotoport=gre | |
38 | rightprotoport=gre | |
39 | ||
40 | """), "gre64": Template("""\ | |
41 | conn $ifname-$version | |
42 | $auth_section | |
43 | leftprotoport=gre | |
44 | rightprotoport=gre | |
45 | ||
46 | """), "geneve": Template("""\ | |
47 | conn $ifname-in-$version | |
48 | $auth_section | |
49 | leftprotoport=udp/6081 | |
50 | rightprotoport=udp | |
51 | ||
52 | conn $ifname-out-$version | |
53 | $auth_section | |
54 | leftprotoport=udp | |
55 | rightprotoport=udp/6081 | |
56 | ||
57 | """), "stt": Template("""\ | |
58 | conn $ifname-in-$version | |
59 | $auth_section | |
60 | leftprotoport=tcp/7471 | |
61 | rightprotoport=tcp | |
62 | ||
63 | conn $ifname-out-$version | |
64 | $auth_section | |
65 | leftprotoport=tcp | |
66 | rightprotoport=tcp/7471 | |
67 | ||
68 | """), "vxlan": Template("""\ | |
69 | conn $ifname-in-$version | |
70 | $auth_section | |
71 | leftprotoport=udp/4789 | |
72 | rightprotoport=udp | |
73 | ||
74 | conn $ifname-out-$version | |
75 | $auth_section | |
76 | leftprotoport=udp | |
77 | rightprotoport=udp/4789 | |
78 | ||
79 | """)} | |
80 | vlog = ovs.vlog.Vlog("ovs-monitor-ipsec") | |
81 | exiting = False | |
82 | monitor = None | |
83 | xfrm = None | |
84 | ||
85 | ||
86 | class 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 | ||
144 | class StrongSwanHelper(object): | |
145 | """This class does StrongSwan specific configurations.""" | |
146 | ||
147 | STRONGSWAN_CONF = """%s | |
148 | charon.plugins.kernel-netlink.set_proto_port_transport_sa = yes | |
149 | charon.plugins.kernel-netlink.xfrm_ack_expires = 10 | |
150 | charon.load_modular = yes | |
151 | charon.plugins.gcm.load = yes | |
152 | """ % (FILE_HEADER) | |
153 | ||
154 | CONF_HEADER = """%s | |
155 | config setup | |
156 | uniqueids=yes | |
157 | ||
158 | conn %%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 | ||
178 | conn prevent_unencrypted_geneve | |
179 | type=drop | |
180 | leftprotoport=udp/6081 | |
181 | mark={0} | |
182 | ||
183 | conn prevent_unencrypted_stt | |
184 | type=drop | |
185 | leftprotoport=tcp/7471 | |
186 | mark={0} | |
187 | ||
188 | conn 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 | ||
368 | class LibreSwanHelper(object): | |
369 | """This class does LibreSwan specific configurations.""" | |
370 | CONF_HEADER = """%s | |
371 | config setup | |
372 | uniqueids=yes | |
373 | ||
374 | conn %%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 | ||
390 | conn prevent_unencrypted_geneve | |
391 | type=drop | |
392 | left=%defaultroute | |
393 | leftprotoport=udp/6081 | |
394 | mark={0} | |
395 | ||
396 | conn prevent_unencrypted_stt | |
397 | type=drop | |
398 | left=%defaultroute | |
399 | leftprotoport=tcp/7471 | |
400 | mark={0} | |
401 | ||
402 | conn 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 | ||
740 | class 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 | ||
911 | class 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 | ||
1107 | def unixctl_xfrm_policies(conn, unused_argv, unused_aux): | |
1108 | global xfrm | |
1109 | policies = xfrm.get_policies() | |
1110 | conn.reply(str(policies)) | |
1111 | ||
1112 | ||
1113 | def unixctl_xfrm_state(conn, unused_argv, unused_aux): | |
1114 | global xfrm | |
1115 | securities = xfrm.get_securities() | |
1116 | conn.reply(str(securities)) | |
1117 | ||
1118 | ||
1119 | def 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 | ||
1125 | def 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 | ||
1133 | def unixctl_refresh(conn, unused_argv, unused_aux): | |
1134 | global monitor | |
1135 | monitor.ike_helper.refresh(monitor) | |
1136 | conn.reply(None) | |
1137 | ||
1138 | ||
1139 | def 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 | ||
1154 | def 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 | ||
1227 | if __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) |