]> git.proxmox.com Git - mirror_ovs.git/blame - debian/ovs-monitor-ipsec
dpif: Add function to get the dpif type.
[mirror_ovs.git] / debian / ovs-monitor-ipsec
CommitLineData
a3acf0b0 1#!/usr/bin/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
JP
35from ovs.db import error
36from ovs.db import types
37import ovs.util
38import ovs.daemon
39import ovs.db.idl
7b2d10c5 40import ovs.unixctl
53cf9963 41import ovs.unixctl.server
27ae98ba 42import ovs.vlog
a3acf0b0 43
27ae98ba 44vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
b54bdbe9 45root_prefix = '' # Prefix for absolute file names, for testing.
38aad449 46SETKEY = "/usr/sbin/setkey"
7b2d10c5
EJ
47exiting = False
48
49
50def unixctl_exit(conn, unused_argv, unused_aux):
51 global exiting
52 exiting = True
53 conn.reply(None)
a3acf0b0 54
0f4d9dce 55
a3acf0b0
JP
56# Class to configure the racoon daemon, which handles IKE negotiation
57class Racoon:
58 # Default locations for files
59 conf_file = "/etc/racoon/racoon.conf"
3c52fa7b 60 cert_dir = "/etc/racoon/certs"
a3acf0b0
JP
61 psk_file = "/etc/racoon/psk.txt"
62
3c52fa7b
JP
63 # Racoon configuration header we use for IKE
64 conf_header = """# Configuration file generated by Open vSwitch
a3acf0b0
JP
65#
66# Do not modify by hand!
67
3c52fa7b
JP
68path pre_shared_key "%s";
69path certificate "%s";
a3acf0b0 70
3c52fa7b
JP
71"""
72
73 # Racoon configuration footer we use for IKE
74 conf_footer = """sainfo anonymous {
75 pfs_group 2;
76 lifetime time 1 hour;
77 encryption_algorithm aes;
78 authentication_algorithm hmac_sha1, hmac_md5;
79 compression_algorithm deflate;
80}
81
82"""
83
84 # Certificate entry template.
85 cert_entry = """remote %s {
a3acf0b0 86 exchange_mode main;
e97a1034 87 nat_traversal on;
73976ebd 88 ike_frag on;
3c52fa7b
JP
89 certificate_type x509 "%s" "%s";
90 my_identifier asn1dn;
91 peers_identifier asn1dn;
92 peers_certfile x509 "%s";
93 verify_identifier on;
a3acf0b0
JP
94 proposal {
95 encryption_algorithm aes;
96 hash_algorithm sha1;
3c52fa7b 97 authentication_method rsasig;
a3acf0b0
JP
98 dh_group 2;
99 }
100}
101
3c52fa7b
JP
102"""
103
104 # Pre-shared key template.
105 psk_entry = """remote %s {
106 exchange_mode main;
107 nat_traversal on;
108 proposal {
109 encryption_algorithm aes;
110 hash_algorithm sha1;
111 authentication_method pre_shared_key;
112 dh_group 2;
113 }
a3acf0b0 114}
3c52fa7b 115
a3acf0b0
JP
116"""
117
118 def __init__(self):
119 self.psk_hosts = {}
120 self.cert_hosts = {}
121
b54bdbe9 122 if not os.path.isdir(root_prefix + self.cert_dir):
ef035eef
JP
123 os.mkdir(self.cert_dir)
124
3c52fa7b 125 # Clean out stale peer certs from previous runs
b54bdbe9
BP
126 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
127 % (root_prefix, self.cert_dir)):
3c52fa7b
JP
128 try:
129 os.remove(ovs_cert)
130 except OSError:
a251af0a 131 vlog.warn("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):
b54bdbe9
BP
137 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
138 "reload"])
a3acf0b0 139 if exitcode != 0:
215d7280 140 # Racoon is finicky about its configuration file and will
3c52fa7b
JP
141 # refuse to start if it sees something it doesn't like
142 # (e.g., a certificate file doesn't exist). Try restarting
143 # the process before giving up.
a251af0a 144 vlog.warn("attempting to restart racoon")
b54bdbe9
BP
145 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
146 "restart"])
3c52fa7b 147 if exitcode != 0:
a251af0a 148 vlog.warn("couldn't reload racoon")
3c52fa7b
JP
149
150 def commit(self):
151 # Rewrite the Racoon configuration file
b54bdbe9 152 conf_file = open(root_prefix + self.conf_file, 'w')
3c52fa7b
JP
153 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
154
155 for host, vals in self.cert_hosts.iteritems():
156 conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
157 vals["private_key"], vals["peer_cert_file"]))
158
159 for host in self.psk_hosts:
160 conf_file.write(Racoon.psk_entry % host)
161
162 conf_file.write(Racoon.conf_footer)
163 conf_file.close()
164
165 # Rewrite the pre-shared keys file; it must only be readable by root.
166 orig_umask = os.umask(0077)
b54bdbe9 167 psk_file = open(root_prefix + Racoon.psk_file, 'w')
3c52fa7b
JP
168 os.umask(orig_umask)
169
170 psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
171 psk_file.write("\n\n")
172 for host, vals in self.psk_hosts.iteritems():
173 psk_file.write("%s %s\n" % (host, vals["psk"]))
174 psk_file.close()
a3acf0b0 175
3c52fa7b 176 self.reload()
a3acf0b0 177
3c52fa7b
JP
178 def _add_psk(self, host, psk):
179 if host in self.cert_hosts:
180 raise error.Error("host %s already defined for cert" % host)
a3acf0b0 181
a3acf0b0 182 self.psk_hosts[host] = psk
3c52fa7b
JP
183 self.commit()
184
185 def _verify_certs(self, vals):
186 # Racoon will refuse to start if the certificate files don't
187 # exist, so verify that they're there.
b54bdbe9 188 if not os.path.isfile(root_prefix + vals["certificate"]):
3c52fa7b
JP
189 raise error.Error("'certificate' file does not exist: %s"
190 % vals["certificate"])
b54bdbe9 191 elif not os.path.isfile(root_prefix + vals["private_key"]):
3c52fa7b
JP
192 raise error.Error("'private_key' file does not exist: %s"
193 % vals["private_key"])
194
195 # Racoon won't start if a given certificate or private key isn't
196 # valid. This is a weak test, but will detect the most flagrant
197 # errors.
198 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
199 raise error.Error("'peer_cert' is not in valid PEM format")
200
b54bdbe9 201 cert = open(root_prefix + vals["certificate"]).read()
3c52fa7b
JP
202 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
203 raise error.Error("'certificate' is not in valid PEM format")
204
b54bdbe9 205 cert = open(root_prefix + vals["private_key"]).read()
3c52fa7b
JP
206 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
207 raise error.Error("'private_key' is not in valid PEM format")
3c52fa7b
JP
208
209 def _add_cert(self, host, vals):
a3acf0b0 210 if host in self.psk_hosts:
3c52fa7b
JP
211 raise error.Error("host %s already defined for psk" % host)
212
ef7ee76a 213 if vals["certificate"] == None:
3c52fa7b 214 raise error.Error("'certificate' not defined for %s" % host)
ef7ee76a 215 elif vals["private_key"] == None:
0f4d9dce 216 # Assume the private key is stored in the same PEM file as
3c52fa7b
JP
217 # the certificate. We make a copy of "vals" so that we don't
218 # modify the original "vals", which would cause the script
219 # to constantly think that the configuration has changed
220 # in the database.
221 vals = vals.copy()
222 vals["private_key"] = vals["certificate"]
223
224 self._verify_certs(vals)
225
226 # The peer's certificate comes to us in PEM format as a string.
227 # Write that string to a file for Racoon to use.
12bb621f 228 f = open(root_prefix + vals["peer_cert_file"], "w")
3c52fa7b
JP
229 f.write(vals["peer_cert"])
230 f.close()
231
3c52fa7b
JP
232 self.cert_hosts[host] = vals
233 self.commit()
234
235 def _del_cert(self, host):
236 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
237 del self.cert_hosts[host]
238 self.commit()
239 try:
b54bdbe9 240 os.remove(root_prefix + peer_cert_file)
3c52fa7b
JP
241 except OSError:
242 pass
243
244 def add_entry(self, host, vals):
245 if vals["peer_cert"]:
246 self._add_cert(host, vals)
247 elif vals["psk"]:
248 self._add_psk(host, vals)
249
250 def del_entry(self, host):
251 if host in self.cert_hosts:
252 self._del_cert(host)
253 elif host in self.psk_hosts:
a3acf0b0 254 del self.psk_hosts[host]
3c52fa7b 255 self.commit()
a3acf0b0
JP
256
257
258# Class to configure IPsec on a system using racoon for IKE and setkey
259# for maintaining the Security Association Database (SAD) and Security
260# Policy Database (SPD). Only policies for GRE are supported.
261class IPsec:
262 def __init__(self):
263 self.sad_flush()
264 self.spd_flush()
265 self.racoon = Racoon()
3c52fa7b 266 self.entries = []
a3acf0b0
JP
267
268 def call_setkey(self, cmds):
269 try:
38aad449 270 p = subprocess.Popen([root_prefix + SETKEY, "-c"],
b54bdbe9
BP
271 stdin=subprocess.PIPE,
272 stdout=subprocess.PIPE)
a3acf0b0 273 except:
38aad449 274 vlog.err("could not call %s%s" % (root_prefix, SETKEY))
a3acf0b0
JP
275 sys.exit(1)
276
277 # xxx It is safer to pass the string into the communicate()
278 # xxx method, but it didn't work for slightly longer commands.
279 # xxx An alternative may need to be found.
280 p.stdin.write(cmds)
281 return p.communicate()[0]
282
283 def get_spi(self, local_ip, remote_ip, proto="esp"):
284 # Run the setkey dump command to retrieve the SAD. Then, parse
285 # the output looking for SPI buried in the output. Note that
286 # multiple SAD entries can exist for the same "flow", since an
287 # older entry could be in a "dying" state.
288 spi_list = []
289 host_line = "%s %s" % (local_ip, remote_ip)
b54bdbe9 290 results = self.call_setkey("dump ;\n").split("\n")
a3acf0b0
JP
291 for i in range(len(results)):
292 if results[i].strip() == host_line:
293 # The SPI is in the line following the host pair
0f4d9dce 294 spi_line = results[i + 1]
a3acf0b0
JP
295 if (spi_line[1:4] == proto):
296 spi = spi_line.split()[2]
297 spi_list.append(spi.split('(')[1].rstrip(')'))
298 return spi_list
299
300 def sad_flush(self):
b54bdbe9 301 self.call_setkey("flush;\n")
a3acf0b0
JP
302
303 def sad_del(self, local_ip, remote_ip):
304 # To delete all SAD entries, we should be able to use setkey's
305 # "deleteall" command. Unfortunately, it's fundamentally broken
306 # on Linux and not documented as such.
307 cmds = ""
308
309 # Delete local_ip->remote_ip SAD entries
310 spi_list = self.get_spi(local_ip, remote_ip)
311 for spi in spi_list:
312 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
313
314 # Delete remote_ip->local_ip SAD entries
315 spi_list = self.get_spi(remote_ip, local_ip)
316 for spi in spi_list:
317 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
318
319 if cmds:
320 self.call_setkey(cmds)
321
322 def spd_flush(self):
b54bdbe9 323 self.call_setkey("spdflush;\n")
a3acf0b0
JP
324
325 def spd_add(self, local_ip, remote_ip):
f916d1cc 326 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
a3acf0b0 327 (local_ip, remote_ip))
b54bdbe9 328 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
a3acf0b0
JP
329 (remote_ip, local_ip))
330 self.call_setkey(cmds)
331
332 def spd_del(self, local_ip, remote_ip):
3c52fa7b 333 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
b54bdbe9 334 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
a3acf0b0
JP
335 self.call_setkey(cmds)
336
3c52fa7b
JP
337 def add_entry(self, local_ip, remote_ip, vals):
338 if remote_ip in self.entries:
339 raise error.Error("host %s already configured for ipsec"
340 % remote_ip)
a3acf0b0 341
3c52fa7b 342 self.racoon.add_entry(remote_ip, vals)
a3acf0b0
JP
343 self.spd_add(local_ip, remote_ip)
344
3c52fa7b 345 self.entries.append(remote_ip)
a3acf0b0 346
3c52fa7b
JP
347 def del_entry(self, local_ip, remote_ip):
348 if remote_ip in self.entries:
349 self.racoon.del_entry(remote_ip)
350 self.spd_del(local_ip, remote_ip)
351 self.sad_del(local_ip, remote_ip)
352
353 self.entries.remove(remote_ip)
a3acf0b0
JP
354
355
3c52fa7b
JP
356def update_ipsec(ipsec, interfaces, new_interfaces):
357 for name, vals in interfaces.iteritems():
358 if name not in new_interfaces:
359 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
360
361 for name, vals in new_interfaces.iteritems():
362 orig_vals = interfaces.get(name)
363 if orig_vals:
364 # Configuration for this host already exists. Check if it's
3831d6f4
JP
365 # changed. We use set difference, since we want to ignore
366 # any local additions to "orig_vals" that we've made
367 # (e.g. the "peer_cert_file" key).
368 if set(vals.items()) - set(orig_vals.items()):
3c52fa7b 369 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
3831d6f4
JP
370 else:
371 continue
3c52fa7b
JP
372
373 try:
374 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
375 except error.Error, msg:
a251af0a 376 vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
3c52fa7b 377
0f4d9dce 378
ef7ee76a 379def get_ssl_cert(data):
8cdf0349 380 for ovs_rec in data["Open_vSwitch"].rows.itervalues():
ad6247f5
BP
381 if ovs_rec.ssl:
382 ssl = ovs_rec.ssl[0]
383 if ssl.certificate and ssl.private_key:
384 return (ssl.certificate, ssl.private_key)
ef7ee76a
JP
385
386 return None
387
0f4d9dce 388
b153e667
EJ
389def main():
390
391 parser = argparse.ArgumentParser()
392 parser.add_argument("database", metavar="DATABASE",
393 help="A socket on which ovsdb-server is listening.")
394 parser.add_argument("--root-prefix", metavar="DIR",
395 help="Use DIR as alternate root directory"
396 " (for testing).")
0f4d9dce 397
27ae98ba 398 ovs.vlog.add_args(parser)
b153e667
EJ
399 ovs.daemon.add_args(parser)
400 args = parser.parse_args()
27ae98ba 401 ovs.vlog.handle_args(args)
b153e667 402 ovs.daemon.handle_args(args)
a3acf0b0 403
b153e667 404 global root_prefix
c4f8424e
EJ
405 if args.root_prefix:
406 root_prefix = args.root_prefix
8cdf0349 407
b153e667 408 remote = args.database
bf42f674
EJ
409 schema_helper = ovs.db.idl.SchemaHelper()
410 schema_helper.register_columns("Interface", ["name", "type", "options"])
411 schema_helper.register_columns("Open_vSwitch", ["ssl"])
412 schema_helper.register_columns("SSL", ["certificate", "private_key"])
413 idl = ovs.db.idl.Idl(remote, schema_helper)
a3acf0b0
JP
414
415 ovs.daemon.daemonize()
416
7b2d10c5 417 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
53cf9963 418 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
7b2d10c5
EJ
419 if error:
420 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
421
a3acf0b0
JP
422 ipsec = IPsec()
423
424 interfaces = {}
6da258aa 425 seqno = idl.change_seqno # Sequence number when we last processed the db
a3acf0b0 426 while True:
7b2d10c5
EJ
427 unixctl_server.run()
428 if exiting:
429 break
430
6da258aa
BP
431 idl.run()
432 if seqno == idl.change_seqno:
a3acf0b0 433 poller = ovs.poller.Poller()
7b2d10c5 434 unixctl_server.wait(poller)
a3acf0b0
JP
435 idl.wait(poller)
436 poller.block()
437 continue
6da258aa 438 seqno = idl.change_seqno
ef7ee76a 439
8cdf0349 440 ssl_cert = get_ssl_cert(idl.tables)
0f4d9dce 441
a3acf0b0 442 new_interfaces = {}
8cdf0349 443 for rec in idl.tables["Interface"].rows.itervalues():
2de795ad 444 if rec.type == "ipsec_gre" or rec.type == "ipsec_gre64":
8cdf0349
BP
445 name = rec.name
446 options = rec.options
12bb621f 447 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
ef7ee76a 448 entry = {
8cdf0349
BP
449 "remote_ip": options.get("remote_ip"),
450 "local_ip": options.get("local_ip", "0.0.0.0/0"),
451 "certificate": options.get("certificate"),
452 "private_key": options.get("private_key"),
453 "use_ssl_cert": options.get("use_ssl_cert"),
454 "peer_cert": options.get("peer_cert"),
12bb621f 455 "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
0f4d9dce 456 "psk": options.get("psk")}
a3acf0b0 457
ef7ee76a 458 if entry["peer_cert"] and entry["psk"]:
a251af0a
EJ
459 vlog.warn("both 'peer_cert' and 'psk' defined for %s"
460 % name)
3c52fa7b 461 continue
ef7ee76a 462 elif not entry["peer_cert"] and not entry["psk"]:
a251af0a 463 vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
3c52fa7b 464 continue
a3acf0b0 465
ef7ee76a
JP
466 # The "use_ssl_cert" option is deprecated and will
467 # likely go away in the near future.
468 if entry["use_ssl_cert"] == "true":
469 if not ssl_cert:
a251af0a 470 vlog.warn("no valid SSL entry for %s" % name)
ef7ee76a
JP
471 continue
472
473 entry["certificate"] = ssl_cert[0]
474 entry["private_key"] = ssl_cert[1]
475
476 new_interfaces[name] = entry
0f4d9dce 477
3c52fa7b
JP
478 if interfaces != new_interfaces:
479 update_ipsec(ipsec, interfaces, new_interfaces)
a3acf0b0 480 interfaces = new_interfaces
0f4d9dce 481
7b2d10c5
EJ
482 unixctl_server.close()
483 idl.close()
484
0f4d9dce 485
a3acf0b0
JP
486if __name__ == '__main__':
487 try:
b153e667 488 main()
a3acf0b0
JP
489 except SystemExit:
490 # Let system.exit() calls complete normally
491 raise
492 except:
27ae98ba 493 vlog.exception("traceback")
55f8a832 494 sys.exit(ovs.daemon.RESTART_EXIT_CODE)