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