]> git.proxmox.com Git - mirror_ovs.git/blame - debian/ovs-monitor-ipsec
debian: Fix build failure installing ovs-vswitchd.conf.db(5) manpage.
[mirror_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
b153e667 28import argparse
3c52fa7b 29import glob
a3acf0b0 30import os
a3acf0b0
JP
31import subprocess
32import sys
33
8cdf0349 34import ovs.dirs
a3acf0b0
JP
35from ovs.db import error
36from ovs.db import types
37import ovs.util
38import ovs.daemon
39import ovs.db.idl
27ae98ba 40import ovs.vlog
a3acf0b0 41
27ae98ba 42vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
b54bdbe9 43root_prefix = '' # Prefix for absolute file names, for testing.
a3acf0b0
JP
44setkey = "/usr/sbin/setkey"
45
0f4d9dce 46
a3acf0b0
JP
47# Class to configure the racoon daemon, which handles IKE negotiation
48class Racoon:
49 # Default locations for files
50 conf_file = "/etc/racoon/racoon.conf"
3c52fa7b 51 cert_dir = "/etc/racoon/certs"
a3acf0b0
JP
52 psk_file = "/etc/racoon/psk.txt"
53
3c52fa7b
JP
54 # Racoon configuration header we use for IKE
55 conf_header = """# Configuration file generated by Open vSwitch
a3acf0b0
JP
56#
57# Do not modify by hand!
58
3c52fa7b
JP
59path pre_shared_key "%s";
60path certificate "%s";
a3acf0b0 61
3c52fa7b
JP
62"""
63
64 # Racoon configuration footer we use for IKE
65 conf_footer = """sainfo anonymous {
66 pfs_group 2;
67 lifetime time 1 hour;
68 encryption_algorithm aes;
69 authentication_algorithm hmac_sha1, hmac_md5;
70 compression_algorithm deflate;
71}
72
73"""
74
75 # Certificate entry template.
76 cert_entry = """remote %s {
a3acf0b0 77 exchange_mode main;
e97a1034 78 nat_traversal on;
73976ebd 79 ike_frag on;
3c52fa7b
JP
80 certificate_type x509 "%s" "%s";
81 my_identifier asn1dn;
82 peers_identifier asn1dn;
83 peers_certfile x509 "%s";
84 verify_identifier on;
a3acf0b0
JP
85 proposal {
86 encryption_algorithm aes;
87 hash_algorithm sha1;
3c52fa7b 88 authentication_method rsasig;
a3acf0b0
JP
89 dh_group 2;
90 }
91}
92
3c52fa7b
JP
93"""
94
95 # Pre-shared key template.
96 psk_entry = """remote %s {
97 exchange_mode main;
98 nat_traversal on;
99 proposal {
100 encryption_algorithm aes;
101 hash_algorithm sha1;
102 authentication_method pre_shared_key;
103 dh_group 2;
104 }
a3acf0b0 105}
3c52fa7b 106
a3acf0b0
JP
107"""
108
109 def __init__(self):
110 self.psk_hosts = {}
111 self.cert_hosts = {}
112
b54bdbe9 113 if not os.path.isdir(root_prefix + self.cert_dir):
ef035eef
JP
114 os.mkdir(self.cert_dir)
115
3c52fa7b 116 # Clean out stale peer certs from previous runs
b54bdbe9
BP
117 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
118 % (root_prefix, self.cert_dir)):
3c52fa7b
JP
119 try:
120 os.remove(ovs_cert)
121 except OSError:
a251af0a 122 vlog.warn("couldn't remove %s" % ovs_cert)
a3acf0b0 123
3c52fa7b
JP
124 # Replace racoon's conf file with our template
125 self.commit()
a3acf0b0
JP
126
127 def reload(self):
b54bdbe9
BP
128 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
129 "reload"])
a3acf0b0 130 if exitcode != 0:
215d7280 131 # Racoon is finicky about its configuration file and will
3c52fa7b
JP
132 # refuse to start if it sees something it doesn't like
133 # (e.g., a certificate file doesn't exist). Try restarting
134 # the process before giving up.
a251af0a 135 vlog.warn("attempting to restart racoon")
b54bdbe9
BP
136 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
137 "restart"])
3c52fa7b 138 if exitcode != 0:
a251af0a 139 vlog.warn("couldn't reload racoon")
3c52fa7b
JP
140
141 def commit(self):
142 # Rewrite the Racoon configuration file
b54bdbe9 143 conf_file = open(root_prefix + self.conf_file, 'w')
3c52fa7b
JP
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)
b54bdbe9 158 psk_file = open(root_prefix + Racoon.psk_file, 'w')
3c52fa7b
JP
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.
b54bdbe9 179 if not os.path.isfile(root_prefix + vals["certificate"]):
3c52fa7b
JP
180 raise error.Error("'certificate' file does not exist: %s"
181 % vals["certificate"])
b54bdbe9 182 elif not os.path.isfile(root_prefix + vals["private_key"]):
3c52fa7b
JP
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
b54bdbe9 192 cert = open(root_prefix + vals["certificate"]).read()
3c52fa7b
JP
193 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
194 raise error.Error("'certificate' is not in valid PEM format")
195
b54bdbe9 196 cert = open(root_prefix + vals["private_key"]).read()
3c52fa7b
JP
197 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
198 raise error.Error("'private_key' is not in valid PEM format")
3c52fa7b
JP
199
200 def _add_cert(self, host, vals):
a3acf0b0 201 if host in self.psk_hosts:
3c52fa7b
JP
202 raise error.Error("host %s already defined for psk" % host)
203
ef7ee76a 204 if vals["certificate"] == None:
3c52fa7b 205 raise error.Error("'certificate' not defined for %s" % host)
ef7ee76a 206 elif vals["private_key"] == None:
0f4d9dce 207 # Assume the private key is stored in the same PEM file as
3c52fa7b
JP
208 # the certificate. We make a copy of "vals" so that we don't
209 # modify the original "vals", which would cause the script
210 # to constantly think that the configuration has changed
211 # in the database.
212 vals = vals.copy()
213 vals["private_key"] = vals["certificate"]
214
215 self._verify_certs(vals)
216
217 # The peer's certificate comes to us in PEM format as a string.
218 # Write that string to a file for Racoon to use.
219 peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
b54bdbe9 220 f = open(root_prefix + peer_cert_file, "w")
3c52fa7b
JP
221 f.write(vals["peer_cert"])
222 f.close()
223
224 vals["peer_cert_file"] = peer_cert_file
225
226 self.cert_hosts[host] = vals
227 self.commit()
228
229 def _del_cert(self, host):
230 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
231 del self.cert_hosts[host]
232 self.commit()
233 try:
b54bdbe9 234 os.remove(root_prefix + peer_cert_file)
3c52fa7b
JP
235 except OSError:
236 pass
237
238 def add_entry(self, host, vals):
239 if vals["peer_cert"]:
240 self._add_cert(host, vals)
241 elif vals["psk"]:
242 self._add_psk(host, vals)
243
244 def del_entry(self, host):
245 if host in self.cert_hosts:
246 self._del_cert(host)
247 elif host in self.psk_hosts:
a3acf0b0 248 del self.psk_hosts[host]
3c52fa7b 249 self.commit()
a3acf0b0
JP
250
251
252# Class to configure IPsec on a system using racoon for IKE and setkey
253# for maintaining the Security Association Database (SAD) and Security
254# Policy Database (SPD). Only policies for GRE are supported.
255class IPsec:
256 def __init__(self):
257 self.sad_flush()
258 self.spd_flush()
259 self.racoon = Racoon()
3c52fa7b 260 self.entries = []
a3acf0b0
JP
261
262 def call_setkey(self, cmds):
263 try:
b54bdbe9
BP
264 p = subprocess.Popen([root_prefix + setkey, "-c"],
265 stdin=subprocess.PIPE,
266 stdout=subprocess.PIPE)
a3acf0b0 267 except:
a251af0a 268 vlog.err("could not call %s%s" % (root_prefix, setkey))
a3acf0b0
JP
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)
b54bdbe9 284 results = self.call_setkey("dump ;\n").split("\n")
a3acf0b0
JP
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
0f4d9dce 288 spi_line = results[i + 1]
a3acf0b0
JP
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):
b54bdbe9 295 self.call_setkey("flush;\n")
a3acf0b0
JP
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):
b54bdbe9 317 self.call_setkey("spdflush;\n")
a3acf0b0
JP
318
319 def spd_add(self, local_ip, remote_ip):
f916d1cc 320 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
a3acf0b0 321 (local_ip, remote_ip))
b54bdbe9 322 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
a3acf0b0
JP
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)
b54bdbe9 328 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
a3acf0b0
JP
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 def del_entry(self, local_ip, remote_ip):
342 if remote_ip in self.entries:
343 self.racoon.del_entry(remote_ip)
344 self.spd_del(local_ip, remote_ip)
345 self.sad_del(local_ip, remote_ip)
346
347 self.entries.remove(remote_ip)
a3acf0b0
JP
348
349
350def keep_table_columns(schema, table_name, column_types):
351 table = schema.tables.get(table_name)
352 if not table:
353 raise error.Error("schema has no %s table" % table_name)
354
355 new_columns = {}
356 for column_name, column_type in column_types.iteritems():
357 column = table.columns.get(column_name)
358 if not column:
359 raise error.Error("%s table schema lacks %s column"
360 % (table_name, column_name))
361 if column.type != column_type:
362 raise error.Error("%s column in %s table has type \"%s\", "
363 "expected type \"%s\""
364 % (column_name, table_name,
365 column.type.toEnglish(),
366 column_type.toEnglish()))
367 new_columns[column_name] = column
368 table.columns = new_columns
369 return table
0f4d9dce
EJ
370
371
8cdf0349 372def prune_schema(schema):
a3acf0b0 373 string_type = types.Type(types.BaseType(types.StringType))
ef7ee76a 374 optional_ssl_type = types.Type(types.BaseType(types.UuidType,
0f4d9dce 375 ref_table_name='SSL'), None, 0, 1)
a3acf0b0
JP
376 string_map_type = types.Type(types.BaseType(types.StringType),
377 types.BaseType(types.StringType),
378 0, sys.maxint)
0f4d9dce 379
a3acf0b0
JP
380 new_tables = {}
381 new_tables["Interface"] = keep_table_columns(
382 schema, "Interface", {"name": string_type,
383 "type": string_type,
e16a28b5 384 "options": string_map_type})
ef7ee76a
JP
385 new_tables["Open_vSwitch"] = keep_table_columns(
386 schema, "Open_vSwitch", {"ssl": optional_ssl_type})
387 new_tables["SSL"] = keep_table_columns(
388 schema, "SSL", {"certificate": string_type,
389 "private_key": string_type})
a3acf0b0
JP
390 schema.tables = new_tables
391
0f4d9dce 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:
a251af0a 411 vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
3c52fa7b 412
0f4d9dce 413
ef7ee76a 414def get_ssl_cert(data):
8cdf0349 415 for ovs_rec in data["Open_vSwitch"].rows.itervalues():
ad6247f5
BP
416 if ovs_rec.ssl:
417 ssl = ovs_rec.ssl[0]
418 if ssl.certificate and ssl.private_key:
419 return (ssl.certificate, ssl.private_key)
ef7ee76a
JP
420
421 return None
422
0f4d9dce 423
b153e667
EJ
424def main():
425
426 parser = argparse.ArgumentParser()
427 parser.add_argument("database", metavar="DATABASE",
428 help="A socket on which ovsdb-server is listening.")
429 parser.add_argument("--root-prefix", metavar="DIR",
430 help="Use DIR as alternate root directory"
431 " (for testing).")
0f4d9dce 432
27ae98ba 433 ovs.vlog.add_args(parser)
b153e667
EJ
434 ovs.daemon.add_args(parser)
435 args = parser.parse_args()
27ae98ba 436 ovs.vlog.handle_args(args)
b153e667 437 ovs.daemon.handle_args(args)
a3acf0b0 438
b153e667 439 global root_prefix
c4f8424e
EJ
440 if args.root_prefix:
441 root_prefix = args.root_prefix
8cdf0349 442
b153e667 443 remote = args.database
8cdf0349
BP
444 schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
445 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
446 prune_schema(schema)
447 idl = ovs.db.idl.Idl(remote, schema)
a3acf0b0
JP
448
449 ovs.daemon.daemonize()
450
451 ipsec = IPsec()
452
453 interfaces = {}
454 while True:
455 if not idl.run():
456 poller = ovs.poller.Poller()
457 idl.wait(poller)
458 poller.block()
459 continue
ef7ee76a 460
8cdf0349 461 ssl_cert = get_ssl_cert(idl.tables)
0f4d9dce 462
a3acf0b0 463 new_interfaces = {}
8cdf0349
BP
464 for rec in idl.tables["Interface"].rows.itervalues():
465 if rec.type == "ipsec_gre":
466 name = rec.name
467 options = rec.options
ef7ee76a 468 entry = {
8cdf0349
BP
469 "remote_ip": options.get("remote_ip"),
470 "local_ip": options.get("local_ip", "0.0.0.0/0"),
471 "certificate": options.get("certificate"),
472 "private_key": options.get("private_key"),
473 "use_ssl_cert": options.get("use_ssl_cert"),
474 "peer_cert": options.get("peer_cert"),
0f4d9dce 475 "psk": options.get("psk")}
a3acf0b0 476
ef7ee76a 477 if entry["peer_cert"] and entry["psk"]:
a251af0a
EJ
478 vlog.warn("both 'peer_cert' and 'psk' defined for %s"
479 % name)
3c52fa7b 480 continue
ef7ee76a 481 elif not entry["peer_cert"] and not entry["psk"]:
a251af0a 482 vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
3c52fa7b 483 continue
a3acf0b0 484
ef7ee76a
JP
485 # The "use_ssl_cert" option is deprecated and will
486 # likely go away in the near future.
487 if entry["use_ssl_cert"] == "true":
488 if not ssl_cert:
a251af0a 489 vlog.warn("no valid SSL entry for %s" % name)
ef7ee76a
JP
490 continue
491
492 entry["certificate"] = ssl_cert[0]
493 entry["private_key"] = ssl_cert[1]
494
495 new_interfaces[name] = entry
0f4d9dce 496
3c52fa7b
JP
497 if interfaces != new_interfaces:
498 update_ipsec(ipsec, interfaces, new_interfaces)
a3acf0b0 499 interfaces = new_interfaces
0f4d9dce
EJ
500
501
a3acf0b0
JP
502if __name__ == '__main__':
503 try:
b153e667 504 main()
a3acf0b0
JP
505 except SystemExit:
506 # Let system.exit() calls complete normally
507 raise
508 except:
27ae98ba 509 vlog.exception("traceback")
55f8a832 510 sys.exit(ovs.daemon.RESTART_EXIT_CODE)