]> git.proxmox.com Git - ovs.git/blame - debian/ovs-monitor-ipsec
daemon: Tolerate EINTR in fork_and_wait_for_startup().
[ovs.git] / debian / ovs-monitor-ipsec
CommitLineData
a3acf0b0
JP
1#!/usr/bin/python
2# Copyright (c) 2009, 2010 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
3c52fa7b
JP
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.
a3acf0b0
JP
26
27
28import getopt
3c52fa7b 29import glob
a3acf0b0
JP
30import logging, logging.handlers
31import os
a3acf0b0
JP
32import subprocess
33import sys
34
35from ovs.db import error
36from ovs.db import types
37import ovs.util
38import ovs.daemon
39import ovs.db.idl
40
41
42# By default log messages as DAEMON into syslog
43s_log = logging.getLogger("ovs-monitor-ipsec")
44l_handler = logging.handlers.SysLogHandler(
45 "/dev/log",
46 facility=logging.handlers.SysLogHandler.LOG_DAEMON)
47l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s')
48l_handler.setFormatter(l_formatter)
49s_log.addHandler(l_handler)
50
51
52setkey = "/usr/sbin/setkey"
53
54# Class to configure the racoon daemon, which handles IKE negotiation
55class Racoon:
56 # Default locations for files
57 conf_file = "/etc/racoon/racoon.conf"
3c52fa7b 58 cert_dir = "/etc/racoon/certs"
a3acf0b0
JP
59 psk_file = "/etc/racoon/psk.txt"
60
3c52fa7b
JP
61 # Racoon configuration header we use for IKE
62 conf_header = """# Configuration file generated by Open vSwitch
a3acf0b0
JP
63#
64# Do not modify by hand!
65
3c52fa7b
JP
66path pre_shared_key "%s";
67path certificate "%s";
a3acf0b0 68
3c52fa7b
JP
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 {
a3acf0b0 84 exchange_mode main;
e97a1034 85 nat_traversal on;
3c52fa7b
JP
86 certificate_type x509 "%s" "%s";
87 my_identifier asn1dn;
88 peers_identifier asn1dn;
89 peers_certfile x509 "%s";
90 verify_identifier on;
a3acf0b0
JP
91 proposal {
92 encryption_algorithm aes;
93 hash_algorithm sha1;
3c52fa7b 94 authentication_method rsasig;
a3acf0b0
JP
95 dh_group 2;
96 }
97}
98
3c52fa7b
JP
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 }
a3acf0b0 111}
3c52fa7b 112
a3acf0b0
JP
113"""
114
115 def __init__(self):
116 self.psk_hosts = {}
117 self.cert_hosts = {}
118
ef035eef
JP
119 if not os.path.isdir(self.cert_dir):
120 os.mkdir(self.cert_dir)
121
3c52fa7b
JP
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)
a3acf0b0 128
3c52fa7b
JP
129 # Replace racoon's conf file with our template
130 self.commit()
a3acf0b0
JP
131
132 def reload(self):
133 exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
134 if exitcode != 0:
3c52fa7b
JP
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()
a3acf0b0 169
3c52fa7b 170 self.reload()
a3acf0b0 171
3c52fa7b
JP
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)
a3acf0b0 175
a3acf0b0 176 self.psk_hosts[host] = psk
3c52fa7b
JP
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):
a3acf0b0 205 if host in self.psk_hosts:
3c52fa7b
JP
206 raise error.Error("host %s already defined for psk" % host)
207
ef7ee76a 208 if vals["certificate"] == None:
3c52fa7b 209 raise error.Error("'certificate' not defined for %s" % host)
ef7ee76a 210 elif vals["private_key"] == None:
3c52fa7b
JP
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:
a3acf0b0 252 del self.psk_hosts[host]
3c52fa7b 253 self.commit()
a3acf0b0
JP
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.
259class IPsec:
260 def __init__(self):
261 self.sad_flush()
262 self.spd_flush()
263 self.racoon = Racoon()
3c52fa7b 264 self.entries = []
a3acf0b0
JP
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):
f916d1cc 323 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
a3acf0b0 324 (local_ip, remote_ip))
f916d1cc 325 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;" %
a3acf0b0
JP
326 (remote_ip, local_ip))
327 self.call_setkey(cmds)
328
329 def spd_del(self, local_ip, remote_ip):
3c52fa7b 330 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
a3acf0b0
JP
331 cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
332 self.call_setkey(cmds)
333
3c52fa7b
JP
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)
a3acf0b0 338
3c52fa7b 339 self.racoon.add_entry(remote_ip, vals)
a3acf0b0
JP
340 self.spd_add(local_ip, remote_ip)
341
3c52fa7b 342 self.entries.append(remote_ip)
a3acf0b0 343
3c52fa7b
JP
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)
a3acf0b0
JP
352
353
354def 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
375def monitor_uuid_schema_cb(schema):
376 string_type = types.Type(types.BaseType(types.StringType))
ef7ee76a
JP
377 optional_ssl_type = types.Type(types.BaseType(types.UuidType,
378 ref_table='SSL'), None, 0, 1)
a3acf0b0
JP
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,
e16a28b5 387 "options": string_map_type})
ef7ee76a
JP
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})
a3acf0b0
JP
393 schema.tables = new_tables
394
395def 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
3c52fa7b
JP
403def 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
ef7ee76a
JP
423def 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
a3acf0b0
JP
432def 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 ovs.daemon.die_if_already_running()
454
455 remote = args[0]
456 idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
457
458 ovs.daemon.daemonize()
459
460 ipsec = IPsec()
461
462 interfaces = {}
463 while True:
464 if not idl.run():
465 poller = ovs.poller.Poller()
466 idl.wait(poller)
467 poller.block()
468 continue
ef7ee76a
JP
469
470 ssl_cert = get_ssl_cert(idl.data)
a3acf0b0
JP
471
472 new_interfaces = {}
473 for rec in idl.data["Interface"].itervalues():
e16a28b5 474 if rec.type.as_scalar() == "ipsec_gre":
3c52fa7b 475 name = rec.name.as_scalar()
ef7ee76a
JP
476 entry = {
477 "remote_ip": rec.options.get("remote_ip"),
478 "local_ip": rec.options.get("local_ip", "0.0.0.0/0"),
479 "certificate": rec.options.get("certificate"),
480 "private_key": rec.options.get("private_key"),
481 "use_ssl_cert": rec.options.get("use_ssl_cert"),
482 "peer_cert": rec.options.get("peer_cert"),
483 "psk": rec.options.get("psk") }
a3acf0b0 484
ef7ee76a 485 if entry["peer_cert"] and entry["psk"]:
3c52fa7b
JP
486 s_log.warning("both 'peer_cert' and 'psk' defined for %s"
487 % name)
488 continue
ef7ee76a 489 elif not entry["peer_cert"] and not entry["psk"]:
3c52fa7b
JP
490 s_log.warning("no 'peer_cert' or 'psk' defined for %s"
491 % name)
492 continue
a3acf0b0 493
ef7ee76a
JP
494 # The "use_ssl_cert" option is deprecated and will
495 # likely go away in the near future.
496 if entry["use_ssl_cert"] == "true":
497 if not ssl_cert:
498 s_log.warning("no valid SSL entry for %s" % name)
499 continue
500
501 entry["certificate"] = ssl_cert[0]
502 entry["private_key"] = ssl_cert[1]
503
504 new_interfaces[name] = entry
3c52fa7b
JP
505
506 if interfaces != new_interfaces:
507 update_ipsec(ipsec, interfaces, new_interfaces)
a3acf0b0
JP
508 interfaces = new_interfaces
509
510if __name__ == '__main__':
511 try:
512 main(sys.argv)
513 except SystemExit:
514 # Let system.exit() calls complete normally
515 raise
516 except:
517 s_log.exception("traceback")
55f8a832 518 sys.exit(ovs.daemon.RESTART_EXIT_CODE)