]>
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 | |
b424beca BT |
148 | charon { |
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 | |
163 | config setup | |
164 | uniqueids=yes | |
165 | ||
166 | conn %%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 | ||
186 | conn prevent_unencrypted_geneve | |
187 | type=drop | |
188 | leftprotoport=udp/6081 | |
189 | mark={0} | |
190 | ||
191 | conn prevent_unencrypted_stt | |
192 | type=drop | |
193 | leftprotoport=tcp/7471 | |
194 | mark={0} | |
195 | ||
196 | conn 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 | ||
376 | class LibreSwanHelper(object): | |
377 | """This class does LibreSwan specific configurations.""" | |
378 | CONF_HEADER = """%s | |
379 | config setup | |
380 | uniqueids=yes | |
381 | ||
382 | conn %%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 | ||
398 | conn prevent_unencrypted_geneve | |
399 | type=drop | |
400 | left=%defaultroute | |
401 | leftprotoport=udp/6081 | |
402 | mark={0} | |
403 | ||
404 | conn prevent_unencrypted_stt | |
405 | type=drop | |
406 | left=%defaultroute | |
407 | leftprotoport=tcp/7471 | |
408 | mark={0} | |
409 | ||
410 | conn 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 | ||
748 | class 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 | ||
919 | class 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 | ||
1115 | def unixctl_xfrm_policies(conn, unused_argv, unused_aux): | |
1116 | global xfrm | |
1117 | policies = xfrm.get_policies() | |
1118 | conn.reply(str(policies)) | |
1119 | ||
1120 | ||
1121 | def unixctl_xfrm_state(conn, unused_argv, unused_aux): | |
1122 | global xfrm | |
1123 | securities = xfrm.get_securities() | |
1124 | conn.reply(str(securities)) | |
1125 | ||
1126 | ||
1127 | def 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 | ||
1133 | def 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 | ||
1141 | def unixctl_refresh(conn, unused_argv, unused_aux): | |
1142 | global monitor | |
1143 | monitor.ike_helper.refresh(monitor) | |
1144 | conn.reply(None) | |
1145 | ||
1146 | ||
1147 | def 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 | ||
1162 | def 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 | ||
1235 | if __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) |