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