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