]> git.proxmox.com Git - ovs.git/blob - debian/ovs-monitor-ipsec
Fix copyright statements from commit f1ab6e06
[ovs.git] / debian / ovs-monitor-ipsec
1 #! /usr/bin/env python
2 # Copyright (c) 2009, 2010, 2011, 2012 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
17 # A daemon to monitor attempts to create GRE-over-IPsec tunnels.
18 # Uses racoon and setkey to support the configuration. Assumes that
19 # OVS has complete control over IPsec configuration for the box.
20
21 # xxx To-do:
22 # - Doesn't actually check that Interface is connected to bridge
23 # - If a certificate is badly formed, Racoon will refuse to start. We
24 # should do a better job of verifying certificates are valid before
25 # adding an interface to racoon.conf.
26
27
28 import argparse
29 import glob
30 import os
31 import subprocess
32 import sys
33
34 import ovs.dirs
35 from ovs.db import error
36 import ovs.util
37 import ovs.daemon
38 import ovs.db.idl
39 import ovs.unixctl
40 import ovs.unixctl.server
41 import ovs.vlog
42 from six.moves import range
43 import six
44
45 vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
46 root_prefix = '' # Prefix for absolute file names, for testing.
47 SETKEY = "/usr/sbin/setkey"
48 exiting = False
49
50
51 def unixctl_exit(conn, unused_argv, unused_aux):
52 global exiting
53 exiting = True
54 conn.reply(None)
55
56
57 # Class to configure the racoon daemon, which handles IKE negotiation
58 class Racoon(object):
59 # Default locations for files
60 conf_file = "/etc/racoon/racoon.conf"
61 cert_dir = "/etc/racoon/certs"
62 psk_file = "/etc/racoon/psk.txt"
63
64 # Racoon configuration header we use for IKE
65 conf_header = """# Configuration file generated by Open vSwitch
66 #
67 # Do not modify by hand!
68
69 path pre_shared_key "%s";
70 path certificate "%s";
71
72 """
73
74 # Racoon configuration footer we use for IKE
75 conf_footer = """sainfo anonymous {
76 pfs_group 2;
77 lifetime time 1 hour;
78 encryption_algorithm aes;
79 authentication_algorithm hmac_sha1, hmac_md5;
80 compression_algorithm deflate;
81 }
82
83 """
84
85 # Certificate entry template.
86 cert_entry = """remote %s {
87 exchange_mode main;
88 nat_traversal on;
89 ike_frag on;
90 certificate_type x509 "%s" "%s";
91 my_identifier asn1dn;
92 peers_identifier asn1dn;
93 peers_certfile x509 "%s";
94 verify_identifier on;
95 proposal {
96 encryption_algorithm aes;
97 hash_algorithm sha1;
98 authentication_method rsasig;
99 dh_group 2;
100 }
101 }
102
103 """
104
105 # Pre-shared key template.
106 psk_entry = """remote %s {
107 exchange_mode main;
108 nat_traversal on;
109 proposal {
110 encryption_algorithm aes;
111 hash_algorithm sha1;
112 authentication_method pre_shared_key;
113 dh_group 2;
114 }
115 }
116
117 """
118
119 def __init__(self):
120 self.psk_hosts = {}
121 self.cert_hosts = {}
122
123 if not os.path.isdir(root_prefix + self.cert_dir):
124 os.mkdir(self.cert_dir)
125
126 # Clean out stale peer certs from previous runs
127 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
128 % (root_prefix, self.cert_dir)):
129 try:
130 os.remove(ovs_cert)
131 except OSError:
132 vlog.warn("couldn't remove %s" % ovs_cert)
133
134 # Replace racoon's conf file with our template
135 self.commit()
136
137 def reload(self):
138 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
139 "reload"])
140 if exitcode != 0:
141 # Racoon is finicky about its configuration file and will
142 # refuse to start if it sees something it doesn't like
143 # (e.g., a certificate file doesn't exist). Try restarting
144 # the process before giving up.
145 vlog.warn("attempting to restart racoon")
146 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
147 "restart"])
148 if exitcode != 0:
149 vlog.warn("couldn't reload racoon")
150
151 def commit(self):
152 # Rewrite the Racoon configuration file
153 conf_file = open(root_prefix + self.conf_file, 'w')
154 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
155
156 for host, vals in six.iteritems(self.cert_hosts):
157 conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
158 vals["private_key"], vals["peer_cert_file"]))
159
160 for host in self.psk_hosts:
161 conf_file.write(Racoon.psk_entry % host)
162
163 conf_file.write(Racoon.conf_footer)
164 conf_file.close()
165
166 # Rewrite the pre-shared keys file; it must only be readable by root.
167 orig_umask = os.umask(0o077)
168 psk_file = open(root_prefix + Racoon.psk_file, 'w')
169 os.umask(orig_umask)
170
171 psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
172 psk_file.write("\n\n")
173 for host, vals in six.iteritems(self.psk_hosts):
174 psk_file.write("%s %s\n" % (host, vals["psk"]))
175 psk_file.close()
176
177 self.reload()
178
179 def _add_psk(self, host, psk):
180 if host in self.cert_hosts:
181 raise error.Error("host %s already defined for cert" % host)
182
183 self.psk_hosts[host] = psk
184 self.commit()
185
186 def _verify_certs(self, vals):
187 # Racoon will refuse to start if the certificate files don't
188 # exist, so verify that they're there.
189 if not os.path.isfile(root_prefix + vals["certificate"]):
190 raise error.Error("'certificate' file does not exist: %s"
191 % vals["certificate"])
192 elif not os.path.isfile(root_prefix + vals["private_key"]):
193 raise error.Error("'private_key' file does not exist: %s"
194 % vals["private_key"])
195
196 # Racoon won't start if a given certificate or private key isn't
197 # valid. This is a weak test, but will detect the most flagrant
198 # errors.
199 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
200 raise error.Error("'peer_cert' is not in valid PEM format")
201
202 cert = open(root_prefix + vals["certificate"]).read()
203 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
204 raise error.Error("'certificate' is not in valid PEM format")
205
206 cert = open(root_prefix + vals["private_key"]).read()
207 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
208 raise error.Error("'private_key' is not in valid PEM format")
209
210 def _add_cert(self, host, vals):
211 if host in self.psk_hosts:
212 raise error.Error("host %s already defined for psk" % host)
213
214 if vals["certificate"] is None:
215 raise error.Error("'certificate' not defined for %s" % host)
216 elif vals["private_key"] is None:
217 # Assume the private key is stored in the same PEM file as
218 # the certificate. We make a copy of "vals" so that we don't
219 # modify the original "vals", which would cause the script
220 # to constantly think that the configuration has changed
221 # in the database.
222 vals = vals.copy()
223 vals["private_key"] = vals["certificate"]
224
225 self._verify_certs(vals)
226
227 # The peer's certificate comes to us in PEM format as a string.
228 # Write that string to a file for Racoon to use.
229 f = open(root_prefix + vals["peer_cert_file"], "w")
230 f.write(vals["peer_cert"])
231 f.close()
232
233 self.cert_hosts[host] = vals
234 self.commit()
235
236 def _del_cert(self, host):
237 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
238 del self.cert_hosts[host]
239 self.commit()
240 try:
241 os.remove(root_prefix + peer_cert_file)
242 except OSError:
243 pass
244
245 def add_entry(self, host, vals):
246 if vals["peer_cert"]:
247 self._add_cert(host, vals)
248 elif vals["psk"]:
249 self._add_psk(host, vals)
250
251 def del_entry(self, host):
252 if host in self.cert_hosts:
253 self._del_cert(host)
254 elif host in self.psk_hosts:
255 del self.psk_hosts[host]
256 self.commit()
257
258
259 # Class to configure IPsec on a system using racoon for IKE and setkey
260 # for maintaining the Security Association Database (SAD) and Security
261 # Policy Database (SPD). Only policies for GRE are supported.
262 class IPsec(object):
263 def __init__(self):
264 self.sad_flush()
265 self.spd_flush()
266 self.racoon = Racoon()
267 self.entries = []
268
269 def call_setkey(self, cmds):
270 try:
271 p = subprocess.Popen([root_prefix + SETKEY, "-c"],
272 stdin=subprocess.PIPE,
273 stdout=subprocess.PIPE)
274 except:
275 vlog.err("could not call %s%s" % (root_prefix, SETKEY))
276 sys.exit(1)
277
278 # xxx It is safer to pass the string into the communicate()
279 # xxx method, but it didn't work for slightly longer commands.
280 # xxx An alternative may need to be found.
281 p.stdin.write(cmds)
282 return p.communicate()[0]
283
284 def get_spi(self, local_ip, remote_ip, proto="esp"):
285 # Run the setkey dump command to retrieve the SAD. Then, parse
286 # the output looking for SPI buried in the output. Note that
287 # multiple SAD entries can exist for the same "flow", since an
288 # older entry could be in a "dying" state.
289 spi_list = []
290 host_line = "%s %s" % (local_ip, remote_ip)
291 results = self.call_setkey("dump ;\n").split("\n")
292 for i in range(len(results)):
293 if results[i].strip() == host_line:
294 # The SPI is in the line following the host pair
295 spi_line = results[i + 1]
296 if (spi_line[1:4] == proto):
297 spi = spi_line.split()[2]
298 spi_list.append(spi.split('(')[1].rstrip(')'))
299 return spi_list
300
301 def sad_flush(self):
302 self.call_setkey("flush;\n")
303
304 def sad_del(self, local_ip, remote_ip):
305 # To delete all SAD entries, we should be able to use setkey's
306 # "deleteall" command. Unfortunately, it's fundamentally broken
307 # on Linux and not documented as such.
308 cmds = ""
309
310 # Delete local_ip->remote_ip SAD entries
311 spi_list = self.get_spi(local_ip, remote_ip)
312 for spi in spi_list:
313 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
314
315 # Delete remote_ip->local_ip SAD entries
316 spi_list = self.get_spi(remote_ip, local_ip)
317 for spi in spi_list:
318 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
319
320 if cmds:
321 self.call_setkey(cmds)
322
323 def spd_flush(self):
324 self.call_setkey("spdflush;\n")
325
326 def spd_add(self, local_ip, remote_ip):
327 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
328 (local_ip, remote_ip))
329 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
330 (remote_ip, local_ip))
331 self.call_setkey(cmds)
332
333 def spd_del(self, local_ip, remote_ip):
334 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
335 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
336 self.call_setkey(cmds)
337
338 def add_entry(self, local_ip, remote_ip, vals):
339 if remote_ip in self.entries:
340 raise error.Error("host %s already configured for ipsec"
341 % remote_ip)
342
343 self.racoon.add_entry(remote_ip, vals)
344 self.spd_add(local_ip, remote_ip)
345
346 self.entries.append(remote_ip)
347
348 def del_entry(self, local_ip, remote_ip):
349 if remote_ip in self.entries:
350 self.racoon.del_entry(remote_ip)
351 self.spd_del(local_ip, remote_ip)
352 self.sad_del(local_ip, remote_ip)
353
354 self.entries.remove(remote_ip)
355
356
357 def update_ipsec(ipsec, interfaces, new_interfaces):
358 for name, vals in six.iteritems(interfaces):
359 if name not in new_interfaces:
360 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
361
362 for name, vals in six.iteritems(new_interfaces):
363 orig_vals = interfaces.get(name)
364 if orig_vals:
365 # Configuration for this host already exists. Check if it's
366 # changed. We use set difference, since we want to ignore
367 # any local additions to "orig_vals" that we've made
368 # (e.g. the "peer_cert_file" key).
369 if set(vals.items()) - set(orig_vals.items()):
370 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
371 else:
372 continue
373
374 try:
375 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
376 except error.Error as msg:
377 vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
378
379
380 def get_ssl_cert(data):
381 for ovs_rec in data["Open_vSwitch"].rows.values():
382 if ovs_rec.ssl:
383 ssl = ovs_rec.ssl[0]
384 if ssl.certificate and ssl.private_key:
385 return (ssl.certificate, ssl.private_key)
386
387 return None
388
389
390 def main():
391
392 parser = argparse.ArgumentParser()
393 parser.add_argument("database", metavar="DATABASE",
394 help="A socket on which ovsdb-server is listening.")
395 parser.add_argument("--root-prefix", metavar="DIR",
396 help="Use DIR as alternate root directory"
397 " (for testing).")
398
399 ovs.vlog.add_args(parser)
400 ovs.daemon.add_args(parser)
401 args = parser.parse_args()
402 ovs.vlog.handle_args(args)
403 ovs.daemon.handle_args(args)
404
405 global root_prefix
406 if args.root_prefix:
407 root_prefix = args.root_prefix
408
409 remote = args.database
410 schema_helper = ovs.db.idl.SchemaHelper()
411 schema_helper.register_columns("Interface", ["name", "type", "options"])
412 schema_helper.register_columns("Open_vSwitch", ["ssl"])
413 schema_helper.register_columns("SSL", ["certificate", "private_key"])
414 idl = ovs.db.idl.Idl(remote, schema_helper)
415
416 ovs.daemon.daemonize()
417
418 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
419 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
420 if error:
421 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
422
423 ipsec = IPsec()
424
425 interfaces = {}
426 seqno = idl.change_seqno # Sequence number when we last processed the db
427 while True:
428 unixctl_server.run()
429 if exiting:
430 break
431
432 idl.run()
433 if seqno == idl.change_seqno:
434 poller = ovs.poller.Poller()
435 unixctl_server.wait(poller)
436 idl.wait(poller)
437 poller.block()
438 continue
439 seqno = idl.change_seqno
440
441 ssl_cert = get_ssl_cert(idl.tables)
442
443 new_interfaces = {}
444 for rec in six.itervalues(idl.tables["Interface"].rows):
445 if rec.type == "ipsec_gre":
446 name = rec.name
447 options = rec.options
448 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
449 entry = {
450 "remote_ip": options.get("remote_ip"),
451 "local_ip": options.get("local_ip", "0.0.0.0/0"),
452 "certificate": options.get("certificate"),
453 "private_key": options.get("private_key"),
454 "use_ssl_cert": options.get("use_ssl_cert"),
455 "peer_cert": options.get("peer_cert"),
456 "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
457 "psk": options.get("psk")}
458
459 if entry["peer_cert"] and entry["psk"]:
460 vlog.warn("both 'peer_cert' and 'psk' defined for %s"
461 % name)
462 continue
463 elif not entry["peer_cert"] and not entry["psk"]:
464 vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
465 continue
466
467 # The "use_ssl_cert" option is deprecated and will
468 # likely go away in the near future.
469 if entry["use_ssl_cert"] == "true":
470 if not ssl_cert:
471 vlog.warn("no valid SSL entry for %s" % name)
472 continue
473
474 entry["certificate"] = ssl_cert[0]
475 entry["private_key"] = ssl_cert[1]
476
477 new_interfaces[name] = entry
478
479 if interfaces != new_interfaces:
480 update_ipsec(ipsec, interfaces, new_interfaces)
481 interfaces = new_interfaces
482
483 unixctl_server.close()
484 idl.close()
485
486
487 if __name__ == '__main__':
488 try:
489 main()
490 except SystemExit:
491 # Let system.exit() calls complete normally
492 raise
493 except:
494 vlog.exception("traceback")
495 sys.exit(ovs.daemon.RESTART_EXIT_CODE)