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