]> git.proxmox.com Git - ovs.git/blame - debian/ovs-monitor-ipsec
python: Convert dict iterators.
[ovs.git] / debian / ovs-monitor-ipsec
CommitLineData
f6783a7a 1#! /usr/bin/env python
e0edde6f 2# Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc.
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 35from ovs.db import error
a3acf0b0
JP
36import ovs.util
37import ovs.daemon
38import ovs.db.idl
7b2d10c5 39import ovs.unixctl
53cf9963 40import ovs.unixctl.server
27ae98ba 41import ovs.vlog
b3ac2947 42from six.moves import range
cb96c1b2 43import six
a3acf0b0 44
27ae98ba 45vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
b54bdbe9 46root_prefix = '' # Prefix for absolute file names, for testing.
38aad449 47SETKEY = "/usr/sbin/setkey"
7b2d10c5
EJ
48exiting = False
49
50
51def unixctl_exit(conn, unused_argv, unused_aux):
52 global exiting
53 exiting = True
54 conn.reply(None)
a3acf0b0 55
0f4d9dce 56
a3acf0b0
JP
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
b54bdbe9 123 if not os.path.isdir(root_prefix + self.cert_dir):
ef035eef
JP
124 os.mkdir(self.cert_dir)
125
3c52fa7b 126 # Clean out stale peer certs from previous runs
b54bdbe9
BP
127 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
128 % (root_prefix, self.cert_dir)):
3c52fa7b
JP
129 try:
130 os.remove(ovs_cert)
131 except OSError:
a251af0a 132 vlog.warn("couldn't remove %s" % ovs_cert)
a3acf0b0 133
3c52fa7b
JP
134 # Replace racoon's conf file with our template
135 self.commit()
a3acf0b0
JP
136
137 def reload(self):
b54bdbe9
BP
138 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
139 "reload"])
a3acf0b0 140 if exitcode != 0:
215d7280 141 # Racoon is finicky about its configuration file and will
3c52fa7b
JP
142 # refuse to start if it sees something it doesn't like
143 # (e.g., a certificate file doesn't exist). Try restarting
144 # the process before giving up.
a251af0a 145 vlog.warn("attempting to restart racoon")
b54bdbe9
BP
146 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
147 "restart"])
3c52fa7b 148 if exitcode != 0:
a251af0a 149 vlog.warn("couldn't reload racoon")
3c52fa7b
JP
150
151 def commit(self):
152 # Rewrite the Racoon configuration file
b54bdbe9 153 conf_file = open(root_prefix + self.conf_file, 'w')
3c52fa7b
JP
154 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
155
cb96c1b2 156 for host, vals in six.iteritems(self.cert_hosts):
3c52fa7b
JP
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.
56ec0611 167 orig_umask = os.umask(0o077)
b54bdbe9 168 psk_file = open(root_prefix + Racoon.psk_file, 'w')
3c52fa7b
JP
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")
cb96c1b2 173 for host, vals in six.iteritems(self.psk_hosts):
3c52fa7b
JP
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.
b54bdbe9 189 if not os.path.isfile(root_prefix + vals["certificate"]):
3c52fa7b
JP
190 raise error.Error("'certificate' file does not exist: %s"
191 % vals["certificate"])
b54bdbe9 192 elif not os.path.isfile(root_prefix + vals["private_key"]):
3c52fa7b
JP
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
b54bdbe9 202 cert = open(root_prefix + vals["certificate"]).read()
3c52fa7b
JP
203 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
204 raise error.Error("'certificate' is not in valid PEM format")
205
b54bdbe9 206 cert = open(root_prefix + vals["private_key"]).read()
3c52fa7b
JP
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
3c057118 214 if vals["certificate"] is None:
3c52fa7b 215 raise error.Error("'certificate' not defined for %s" % host)
3c057118 216 elif vals["private_key"] is 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.
12bb621f 229 f = open(root_prefix + vals["peer_cert_file"], "w")
3c52fa7b
JP
230 f.write(vals["peer_cert"])
231 f.close()
232
3c52fa7b
JP
233 self.cert_hosts[host] = vals
234 self.commit()
235
236 def _del_cert(self, host):
237 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
238 del self.cert_hosts[host]
239 self.commit()
240 try:
b54bdbe9 241 os.remove(root_prefix + peer_cert_file)
3c52fa7b
JP
242 except OSError:
243 pass
244
245 def add_entry(self, host, vals):
246 if vals["peer_cert"]:
247 self._add_cert(host, vals)
248 elif vals["psk"]:
249 self._add_psk(host, vals)
250
251 def del_entry(self, host):
252 if host in self.cert_hosts:
253 self._del_cert(host)
254 elif host in self.psk_hosts:
a3acf0b0 255 del self.psk_hosts[host]
3c52fa7b 256 self.commit()
a3acf0b0
JP
257
258
259# Class to configure IPsec on a system using racoon for IKE and setkey
260# for maintaining the Security Association Database (SAD) and Security
261# Policy Database (SPD). Only policies for GRE are supported.
262class IPsec:
263 def __init__(self):
264 self.sad_flush()
265 self.spd_flush()
266 self.racoon = Racoon()
3c52fa7b 267 self.entries = []
a3acf0b0
JP
268
269 def call_setkey(self, cmds):
270 try:
38aad449 271 p = subprocess.Popen([root_prefix + SETKEY, "-c"],
b54bdbe9
BP
272 stdin=subprocess.PIPE,
273 stdout=subprocess.PIPE)
a3acf0b0 274 except:
38aad449 275 vlog.err("could not call %s%s" % (root_prefix, SETKEY))
a3acf0b0
JP
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)
b54bdbe9 291 results = self.call_setkey("dump ;\n").split("\n")
a3acf0b0
JP
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
0f4d9dce 295 spi_line = results[i + 1]
a3acf0b0
JP
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):
b54bdbe9 302 self.call_setkey("flush;\n")
a3acf0b0
JP
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):
b54bdbe9 324 self.call_setkey("spdflush;\n")
a3acf0b0
JP
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))
b54bdbe9 329 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
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)
b54bdbe9 335 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
a3acf0b0
JP
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 def del_entry(self, local_ip, remote_ip):
349 if remote_ip in self.entries:
350 self.racoon.del_entry(remote_ip)
351 self.spd_del(local_ip, remote_ip)
352 self.sad_del(local_ip, remote_ip)
353
354 self.entries.remove(remote_ip)
a3acf0b0
JP
355
356
3c52fa7b 357def update_ipsec(ipsec, interfaces, new_interfaces):
cb96c1b2 358 for name, vals in six.iteritems(interfaces):
3c52fa7b
JP
359 if name not in new_interfaces:
360 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
361
cb96c1b2 362 for name, vals in six.iteritems(new_interfaces):
3c52fa7b
JP
363 orig_vals = interfaces.get(name)
364 if orig_vals:
365 # Configuration for this host already exists. Check if it's
3831d6f4
JP
366 # changed. We use set difference, since we want to ignore
367 # any local additions to "orig_vals" that we've made
368 # (e.g. the "peer_cert_file" key).
369 if set(vals.items()) - set(orig_vals.items()):
3c52fa7b 370 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
3831d6f4
JP
371 else:
372 continue
3c52fa7b
JP
373
374 try:
375 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
f3068bff 376 except error.Error as msg:
a251af0a 377 vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
3c52fa7b 378
0f4d9dce 379
ef7ee76a 380def get_ssl_cert(data):
cb96c1b2 381 for ovs_rec in data["Open_vSwitch"].rows.values():
ad6247f5
BP
382 if ovs_rec.ssl:
383 ssl = ovs_rec.ssl[0]
384 if ssl.certificate and ssl.private_key:
385 return (ssl.certificate, ssl.private_key)
ef7ee76a
JP
386
387 return None
388
0f4d9dce 389
b153e667
EJ
390def main():
391
392 parser = argparse.ArgumentParser()
393 parser.add_argument("database", metavar="DATABASE",
394 help="A socket on which ovsdb-server is listening.")
395 parser.add_argument("--root-prefix", metavar="DIR",
396 help="Use DIR as alternate root directory"
397 " (for testing).")
0f4d9dce 398
27ae98ba 399 ovs.vlog.add_args(parser)
b153e667
EJ
400 ovs.daemon.add_args(parser)
401 args = parser.parse_args()
27ae98ba 402 ovs.vlog.handle_args(args)
b153e667 403 ovs.daemon.handle_args(args)
a3acf0b0 404
b153e667 405 global root_prefix
c4f8424e
EJ
406 if args.root_prefix:
407 root_prefix = args.root_prefix
8cdf0349 408
b153e667 409 remote = args.database
bf42f674
EJ
410 schema_helper = ovs.db.idl.SchemaHelper()
411 schema_helper.register_columns("Interface", ["name", "type", "options"])
412 schema_helper.register_columns("Open_vSwitch", ["ssl"])
413 schema_helper.register_columns("SSL", ["certificate", "private_key"])
414 idl = ovs.db.idl.Idl(remote, schema_helper)
a3acf0b0
JP
415
416 ovs.daemon.daemonize()
417
7b2d10c5 418 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
53cf9963 419 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
7b2d10c5
EJ
420 if error:
421 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
422
a3acf0b0
JP
423 ipsec = IPsec()
424
425 interfaces = {}
6da258aa 426 seqno = idl.change_seqno # Sequence number when we last processed the db
a3acf0b0 427 while True:
7b2d10c5
EJ
428 unixctl_server.run()
429 if exiting:
430 break
431
6da258aa
BP
432 idl.run()
433 if seqno == idl.change_seqno:
a3acf0b0 434 poller = ovs.poller.Poller()
7b2d10c5 435 unixctl_server.wait(poller)
a3acf0b0
JP
436 idl.wait(poller)
437 poller.block()
438 continue
6da258aa 439 seqno = idl.change_seqno
ef7ee76a 440
8cdf0349 441 ssl_cert = get_ssl_cert(idl.tables)
0f4d9dce 442
a3acf0b0 443 new_interfaces = {}
cb96c1b2 444 for rec in six.itervalues(idl.tables["Interface"].rows):
99e7b077 445 if rec.type == "ipsec_gre":
8cdf0349
BP
446 name = rec.name
447 options = rec.options
12bb621f 448 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
ef7ee76a 449 entry = {
8cdf0349
BP
450 "remote_ip": options.get("remote_ip"),
451 "local_ip": options.get("local_ip", "0.0.0.0/0"),
452 "certificate": options.get("certificate"),
453 "private_key": options.get("private_key"),
454 "use_ssl_cert": options.get("use_ssl_cert"),
455 "peer_cert": options.get("peer_cert"),
12bb621f 456 "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
0f4d9dce 457 "psk": options.get("psk")}
a3acf0b0 458
ef7ee76a 459 if entry["peer_cert"] and entry["psk"]:
a251af0a
EJ
460 vlog.warn("both 'peer_cert' and 'psk' defined for %s"
461 % name)
3c52fa7b 462 continue
ef7ee76a 463 elif not entry["peer_cert"] and not entry["psk"]:
a251af0a 464 vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
3c52fa7b 465 continue
a3acf0b0 466
ef7ee76a
JP
467 # The "use_ssl_cert" option is deprecated and will
468 # likely go away in the near future.
469 if entry["use_ssl_cert"] == "true":
470 if not ssl_cert:
a251af0a 471 vlog.warn("no valid SSL entry for %s" % name)
ef7ee76a
JP
472 continue
473
474 entry["certificate"] = ssl_cert[0]
475 entry["private_key"] = ssl_cert[1]
476
477 new_interfaces[name] = entry
0f4d9dce 478
3c52fa7b
JP
479 if interfaces != new_interfaces:
480 update_ipsec(ipsec, interfaces, new_interfaces)
a3acf0b0 481 interfaces = new_interfaces
0f4d9dce 482
7b2d10c5
EJ
483 unixctl_server.close()
484 idl.close()
485
0f4d9dce 486
a3acf0b0
JP
487if __name__ == '__main__':
488 try:
b153e667 489 main()
a3acf0b0
JP
490 except SystemExit:
491 # Let system.exit() calls complete normally
492 raise
493 except:
27ae98ba 494 vlog.exception("traceback")
55f8a832 495 sys.exit(ovs.daemon.RESTART_EXIT_CODE)