]> git.proxmox.com Git - ovs.git/blame - debian/ovs-monitor-ipsec
debian: Require ipsec-tools version 0.8~alpha20101208.
[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
3c52fa7b
JP
119 # Clean out stale peer certs from previous runs
120 for ovs_cert in glob.glob("%s/ovs-*.pem" % self.cert_dir):
121 try:
122 os.remove(ovs_cert)
123 except OSError:
124 s_log.warning("couldn't remove %s" % ovs_cert)
a3acf0b0 125
3c52fa7b
JP
126 # Replace racoon's conf file with our template
127 self.commit()
a3acf0b0
JP
128
129 def reload(self):
130 exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
131 if exitcode != 0:
3c52fa7b
JP
132 # Racoon is finicky about it's configuration file and will
133 # refuse to start if it sees something it doesn't like
134 # (e.g., a certificate file doesn't exist). Try restarting
135 # the process before giving up.
136 s_log.warning("attempting to restart racoon")
137 exitcode = subprocess.call(["/etc/init.d/racoon", "restart"])
138 if exitcode != 0:
139 s_log.warning("couldn't reload racoon")
140
141 def commit(self):
142 # Rewrite the Racoon configuration file
143 conf_file = open(self.conf_file, 'w')
144 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
145
146 for host, vals in self.cert_hosts.iteritems():
147 conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
148 vals["private_key"], vals["peer_cert_file"]))
149
150 for host in self.psk_hosts:
151 conf_file.write(Racoon.psk_entry % host)
152
153 conf_file.write(Racoon.conf_footer)
154 conf_file.close()
155
156 # Rewrite the pre-shared keys file; it must only be readable by root.
157 orig_umask = os.umask(0077)
158 psk_file = open(Racoon.psk_file, 'w')
159 os.umask(orig_umask)
160
161 psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
162 psk_file.write("\n\n")
163 for host, vals in self.psk_hosts.iteritems():
164 psk_file.write("%s %s\n" % (host, vals["psk"]))
165 psk_file.close()
a3acf0b0 166
3c52fa7b 167 self.reload()
a3acf0b0 168
3c52fa7b
JP
169 def _add_psk(self, host, psk):
170 if host in self.cert_hosts:
171 raise error.Error("host %s already defined for cert" % host)
a3acf0b0 172
a3acf0b0 173 self.psk_hosts[host] = psk
3c52fa7b
JP
174 self.commit()
175
176 def _verify_certs(self, vals):
177 # Racoon will refuse to start if the certificate files don't
178 # exist, so verify that they're there.
179 if not os.path.isfile(vals["certificate"]):
180 raise error.Error("'certificate' file does not exist: %s"
181 % vals["certificate"])
182 elif not os.path.isfile(vals["private_key"]):
183 raise error.Error("'private_key' file does not exist: %s"
184 % vals["private_key"])
185
186 # Racoon won't start if a given certificate or private key isn't
187 # valid. This is a weak test, but will detect the most flagrant
188 # errors.
189 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
190 raise error.Error("'peer_cert' is not in valid PEM format")
191
192 cert = open(vals["certificate"]).read()
193 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
194 raise error.Error("'certificate' is not in valid PEM format")
195
196 cert = open(vals["private_key"]).read()
197 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
198 raise error.Error("'private_key' is not in valid PEM format")
199
200
201 def _add_cert(self, host, vals):
a3acf0b0 202 if host in self.psk_hosts:
3c52fa7b
JP
203 raise error.Error("host %s already defined for psk" % host)
204
205 if "certificate" not in vals:
206 raise error.Error("'certificate' not defined for %s" % host)
207 elif "private_key" not in vals:
208 # Assume the private key is stored in the same PEM file as
209 # the certificate. We make a copy of "vals" so that we don't
210 # modify the original "vals", which would cause the script
211 # to constantly think that the configuration has changed
212 # in the database.
213 vals = vals.copy()
214 vals["private_key"] = vals["certificate"]
215
216 self._verify_certs(vals)
217
218 # The peer's certificate comes to us in PEM format as a string.
219 # Write that string to a file for Racoon to use.
220 peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
221 f = open(peer_cert_file, "w")
222 f.write(vals["peer_cert"])
223 f.close()
224
225 vals["peer_cert_file"] = peer_cert_file
226
227 self.cert_hosts[host] = vals
228 self.commit()
229
230 def _del_cert(self, host):
231 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
232 del self.cert_hosts[host]
233 self.commit()
234 try:
235 os.remove(peer_cert_file)
236 except OSError:
237 pass
238
239 def add_entry(self, host, vals):
240 if vals["peer_cert"]:
241 self._add_cert(host, vals)
242 elif vals["psk"]:
243 self._add_psk(host, vals)
244
245 def del_entry(self, host):
246 if host in self.cert_hosts:
247 self._del_cert(host)
248 elif host in self.psk_hosts:
a3acf0b0 249 del self.psk_hosts[host]
3c52fa7b 250 self.commit()
a3acf0b0
JP
251
252
253# Class to configure IPsec on a system using racoon for IKE and setkey
254# for maintaining the Security Association Database (SAD) and Security
255# Policy Database (SPD). Only policies for GRE are supported.
256class IPsec:
257 def __init__(self):
258 self.sad_flush()
259 self.spd_flush()
260 self.racoon = Racoon()
3c52fa7b 261 self.entries = []
a3acf0b0
JP
262
263 def call_setkey(self, cmds):
264 try:
265 p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE,
266 stdout=subprocess.PIPE)
267 except:
268 s_log.error("could not call setkey")
269 sys.exit(1)
270
271 # xxx It is safer to pass the string into the communicate()
272 # xxx method, but it didn't work for slightly longer commands.
273 # xxx An alternative may need to be found.
274 p.stdin.write(cmds)
275 return p.communicate()[0]
276
277 def get_spi(self, local_ip, remote_ip, proto="esp"):
278 # Run the setkey dump command to retrieve the SAD. Then, parse
279 # the output looking for SPI buried in the output. Note that
280 # multiple SAD entries can exist for the same "flow", since an
281 # older entry could be in a "dying" state.
282 spi_list = []
283 host_line = "%s %s" % (local_ip, remote_ip)
284 results = self.call_setkey("dump ;").split("\n")
285 for i in range(len(results)):
286 if results[i].strip() == host_line:
287 # The SPI is in the line following the host pair
288 spi_line = results[i+1]
289 if (spi_line[1:4] == proto):
290 spi = spi_line.split()[2]
291 spi_list.append(spi.split('(')[1].rstrip(')'))
292 return spi_list
293
294 def sad_flush(self):
295 self.call_setkey("flush;")
296
297 def sad_del(self, local_ip, remote_ip):
298 # To delete all SAD entries, we should be able to use setkey's
299 # "deleteall" command. Unfortunately, it's fundamentally broken
300 # on Linux and not documented as such.
301 cmds = ""
302
303 # Delete local_ip->remote_ip SAD entries
304 spi_list = self.get_spi(local_ip, remote_ip)
305 for spi in spi_list:
306 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
307
308 # Delete remote_ip->local_ip SAD entries
309 spi_list = self.get_spi(remote_ip, local_ip)
310 for spi in spi_list:
311 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
312
313 if cmds:
314 self.call_setkey(cmds)
315
316 def spd_flush(self):
317 self.call_setkey("spdflush;")
318
319 def spd_add(self, local_ip, remote_ip):
3c52fa7b 320 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;\n" %
a3acf0b0 321 (local_ip, remote_ip))
a3acf0b0
JP
322 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" %
323 (remote_ip, local_ip))
324 self.call_setkey(cmds)
325
326 def spd_del(self, local_ip, remote_ip):
3c52fa7b 327 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
a3acf0b0
JP
328 cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
329 self.call_setkey(cmds)
330
3c52fa7b
JP
331 def add_entry(self, local_ip, remote_ip, vals):
332 if remote_ip in self.entries:
333 raise error.Error("host %s already configured for ipsec"
334 % remote_ip)
a3acf0b0 335
3c52fa7b 336 self.racoon.add_entry(remote_ip, vals)
a3acf0b0
JP
337 self.spd_add(local_ip, remote_ip)
338
3c52fa7b 339 self.entries.append(remote_ip)
a3acf0b0 340
3c52fa7b
JP
341
342 def del_entry(self, local_ip, remote_ip):
343 if remote_ip in self.entries:
344 self.racoon.del_entry(remote_ip)
345 self.spd_del(local_ip, remote_ip)
346 self.sad_del(local_ip, remote_ip)
347
348 self.entries.remove(remote_ip)
a3acf0b0
JP
349
350
351def keep_table_columns(schema, table_name, column_types):
352 table = schema.tables.get(table_name)
353 if not table:
354 raise error.Error("schema has no %s table" % table_name)
355
356 new_columns = {}
357 for column_name, column_type in column_types.iteritems():
358 column = table.columns.get(column_name)
359 if not column:
360 raise error.Error("%s table schema lacks %s column"
361 % (table_name, column_name))
362 if column.type != column_type:
363 raise error.Error("%s column in %s table has type \"%s\", "
364 "expected type \"%s\""
365 % (column_name, table_name,
366 column.type.toEnglish(),
367 column_type.toEnglish()))
368 new_columns[column_name] = column
369 table.columns = new_columns
370 return table
371
372def monitor_uuid_schema_cb(schema):
373 string_type = types.Type(types.BaseType(types.StringType))
374 string_map_type = types.Type(types.BaseType(types.StringType),
375 types.BaseType(types.StringType),
376 0, sys.maxint)
377
378 new_tables = {}
379 new_tables["Interface"] = keep_table_columns(
380 schema, "Interface", {"name": string_type,
381 "type": string_type,
e16a28b5 382 "options": string_map_type})
a3acf0b0
JP
383 schema.tables = new_tables
384
385def usage():
386 print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
387 print "where DATABASE is a socket on which ovsdb-server is listening."
388 ovs.daemon.usage()
389 print "Other options:"
390 print " -h, --help display this help message"
391 sys.exit(0)
392
3c52fa7b
JP
393def update_ipsec(ipsec, interfaces, new_interfaces):
394 for name, vals in interfaces.iteritems():
395 if name not in new_interfaces:
396 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
397
398 for name, vals in new_interfaces.iteritems():
399 orig_vals = interfaces.get(name)
400 if orig_vals:
401 # Configuration for this host already exists. Check if it's
402 # changed.
403 if vals == orig_vals:
404 continue
405 else:
406 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
407
408 try:
409 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
410 except error.Error, msg:
411 s_log.warning("skipping ipsec config for %s: %s" % (name, msg))
412
a3acf0b0
JP
413def main(argv):
414 try:
415 options, args = getopt.gnu_getopt(
416 argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS)
417 except getopt.GetoptError, geo:
418 sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg))
419 sys.exit(1)
420
421 for key, value in options:
422 if key in ['-h', '--help']:
423 usage()
424 elif not ovs.daemon.parse_opt(key, value):
425 sys.stderr.write("%s: unhandled option %s\n"
426 % (ovs.util.PROGRAM_NAME, key))
427 sys.exit(1)
428
429 if len(args) != 1:
430 sys.stderr.write("%s: exactly one nonoption argument is required "
431 "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
432 sys.exit(1)
433
434 ovs.daemon.die_if_already_running()
435
436 remote = args[0]
437 idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
438
439 ovs.daemon.daemonize()
440
441 ipsec = IPsec()
442
443 interfaces = {}
444 while True:
445 if not idl.run():
446 poller = ovs.poller.Poller()
447 idl.wait(poller)
448 poller.block()
449 continue
450
451 new_interfaces = {}
452 for rec in idl.data["Interface"].itervalues():
e16a28b5 453 if rec.type.as_scalar() == "ipsec_gre":
3c52fa7b
JP
454 name = rec.name.as_scalar()
455 peer_cert = rec.options.get("peer_cert")
456 psk = rec.options.get("psk")
a3acf0b0 457
3c52fa7b
JP
458 if peer_cert and psk:
459 s_log.warning("both 'peer_cert' and 'psk' defined for %s"
460 % name)
461 continue
462 elif not peer_cert and not psk:
463 s_log.warning("no 'peer_cert' or 'psk' defined for %s"
464 % name)
465 continue
a3acf0b0 466
3c52fa7b
JP
467 new_interfaces[name] = {
468 "remote_ip": rec.options.get("remote_ip"),
469 "local_ip": rec.options.get("local_ip", "0.0.0.0/0"),
470 "certificate": rec.options.get("certificate"),
471 "private_key": rec.options.get("private_key"),
472 "peer_cert": peer_cert,
473 "psk": psk }
474
475 if interfaces != new_interfaces:
476 update_ipsec(ipsec, interfaces, new_interfaces)
a3acf0b0
JP
477 interfaces = new_interfaces
478
479if __name__ == '__main__':
480 try:
481 main(sys.argv)
482 except SystemExit:
483 # Let system.exit() calls complete normally
484 raise
485 except:
486 s_log.exception("traceback")
55f8a832 487 sys.exit(ovs.daemon.RESTART_EXIT_CODE)