2 # Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
22 # - Doesn't actually check that Interface is connected to bridge
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.
35 from ovs.db import error
36 from ovs.db import types
41 import ovs.unixctl.server
44 vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
45 root_prefix = '' # Prefix for absolute file names, for testing.
46 SETKEY = "/usr/sbin/setkey"
50 def unixctl_exit(conn, unused_argv, unused_aux):
56 # Class to configure the racoon daemon, which handles IKE negotiation
58 # Default locations for files
59 conf_file = "/etc/racoon/racoon.conf"
60 cert_dir = "/etc/racoon/certs"
61 psk_file = "/etc/racoon/psk.txt"
63 # Racoon configuration header we use for IKE
64 conf_header = """# Configuration file generated by Open vSwitch
66 # Do not modify by hand!
68 path pre_shared_key "%s";
69 path certificate "%s";
73 # Racoon configuration footer we use for IKE
74 conf_footer = """sainfo anonymous {
77 encryption_algorithm aes;
78 authentication_algorithm hmac_sha1, hmac_md5;
79 compression_algorithm deflate;
84 # Certificate entry template.
85 cert_entry = """remote %s {
89 certificate_type x509 "%s" "%s";
91 peers_identifier asn1dn;
92 peers_certfile x509 "%s";
95 encryption_algorithm aes;
97 authentication_method rsasig;
104 # Pre-shared key template.
105 psk_entry = """remote %s {
109 encryption_algorithm aes;
111 authentication_method pre_shared_key;
122 if not os.path.isdir(root_prefix + self.cert_dir):
123 os.mkdir(self.cert_dir)
125 # Clean out stale peer certs from previous runs
126 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
127 % (root_prefix, self.cert_dir)):
131 vlog.warn("couldn't remove %s" % ovs_cert)
133 # Replace racoon's conf file with our template
137 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
140 # Racoon is finicky about its configuration file and will
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.
144 vlog.warn("attempting to restart racoon")
145 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
148 vlog.warn("couldn't reload racoon")
151 # Rewrite the Racoon configuration file
152 conf_file = open(root_prefix + self.conf_file, 'w')
153 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
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"]))
159 for host in self.psk_hosts:
160 conf_file.write(Racoon.psk_entry % host)
162 conf_file.write(Racoon.conf_footer)
165 # Rewrite the pre-shared keys file; it must only be readable by root.
166 orig_umask = os.umask(0077)
167 psk_file = open(root_prefix + Racoon.psk_file, 'w')
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"]))
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)
182 self.psk_hosts[host] = psk
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.
188 if not os.path.isfile(root_prefix + vals["certificate"]):
189 raise error.Error("'certificate' file does not exist: %s"
190 % vals["certificate"])
191 elif not os.path.isfile(root_prefix + vals["private_key"]):
192 raise error.Error("'private_key' file does not exist: %s"
193 % vals["private_key"])
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
198 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
199 raise error.Error("'peer_cert' is not in valid PEM format")
201 cert = open(root_prefix + vals["certificate"]).read()
202 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
203 raise error.Error("'certificate' is not in valid PEM format")
205 cert = open(root_prefix + vals["private_key"]).read()
206 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
207 raise error.Error("'private_key' is not in valid PEM format")
209 def _add_cert(self, host, vals):
210 if host in self.psk_hosts:
211 raise error.Error("host %s already defined for psk" % host)
213 if vals["certificate"] == None:
214 raise error.Error("'certificate' not defined for %s" % host)
215 elif vals["private_key"] == None:
216 # Assume the private key is stored in the same PEM file as
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
222 vals["private_key"] = vals["certificate"]
224 self._verify_certs(vals)
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.
228 f = open(root_prefix + vals["peer_cert_file"], "w")
229 f.write(vals["peer_cert"])
232 self.cert_hosts[host] = vals
235 def _del_cert(self, host):
236 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
237 del self.cert_hosts[host]
240 os.remove(root_prefix + peer_cert_file)
244 def add_entry(self, host, vals):
245 if vals["peer_cert"]:
246 self._add_cert(host, vals)
248 self._add_psk(host, vals)
250 def del_entry(self, host):
251 if host in self.cert_hosts:
253 elif host in self.psk_hosts:
254 del self.psk_hosts[host]
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.
265 self.racoon = Racoon()
268 def call_setkey(self, cmds):
270 p = subprocess.Popen([root_prefix + SETKEY, "-c"],
271 stdin=subprocess.PIPE,
272 stdout=subprocess.PIPE)
274 vlog.err("could not call %s%s" % (root_prefix, SETKEY))
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.
281 return p.communicate()[0]
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.
289 host_line = "%s %s" % (local_ip, remote_ip)
290 results = self.call_setkey("dump ;\n").split("\n")
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
294 spi_line = results[i + 1]
295 if (spi_line[1:4] == proto):
296 spi = spi_line.split()[2]
297 spi_list.append(spi.split('(')[1].rstrip(')'))
301 self.call_setkey("flush;\n")
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.
309 # Delete local_ip->remote_ip SAD entries
310 spi_list = self.get_spi(local_ip, remote_ip)
312 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
314 # Delete remote_ip->local_ip SAD entries
315 spi_list = self.get_spi(remote_ip, local_ip)
317 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
320 self.call_setkey(cmds)
323 self.call_setkey("spdflush;\n")
325 def spd_add(self, local_ip, remote_ip):
326 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
327 (local_ip, remote_ip))
328 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
329 (remote_ip, local_ip))
330 self.call_setkey(cmds)
332 def spd_del(self, local_ip, remote_ip):
333 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
334 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
335 self.call_setkey(cmds)
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"
342 self.racoon.add_entry(remote_ip, vals)
343 self.spd_add(local_ip, remote_ip)
345 self.entries.append(remote_ip)
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)
353 self.entries.remove(remote_ip)
356 def 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"])
361 for name, vals in new_interfaces.iteritems():
362 orig_vals = interfaces.get(name)
364 # Configuration for this host already exists. Check if it's
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()):
369 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
374 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
375 except error.Error, msg:
376 vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
379 def get_ssl_cert(data):
380 for ovs_rec in data["Open_vSwitch"].rows.itervalues():
383 if ssl.certificate and ssl.private_key:
384 return (ssl.certificate, ssl.private_key)
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"
398 ovs.vlog.add_args(parser)
399 ovs.daemon.add_args(parser)
400 args = parser.parse_args()
401 ovs.vlog.handle_args(args)
402 ovs.daemon.handle_args(args)
406 root_prefix = args.root_prefix
408 remote = args.database
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)
415 ovs.daemon.daemonize()
417 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
418 error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
420 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
425 seqno = idl.change_seqno # Sequence number when we last processed the db
432 if seqno == idl.change_seqno:
433 poller = ovs.poller.Poller()
434 unixctl_server.wait(poller)
438 seqno = idl.change_seqno
440 ssl_cert = get_ssl_cert(idl.tables)
443 for rec in idl.tables["Interface"].rows.itervalues():
444 if rec.type == "ipsec_gre":
446 options = rec.options
447 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
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"),
455 "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
456 "psk": options.get("psk")}
458 if entry["peer_cert"] and entry["psk"]:
459 vlog.warn("both 'peer_cert' and 'psk' defined for %s"
462 elif not entry["peer_cert"] and not entry["psk"]:
463 vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
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":
470 vlog.warn("no valid SSL entry for %s" % name)
473 entry["certificate"] = ssl_cert[0]
474 entry["private_key"] = ssl_cert[1]
476 new_interfaces[name] = entry
478 if interfaces != new_interfaces:
479 update_ipsec(ipsec, interfaces, new_interfaces)
480 interfaces = new_interfaces
482 unixctl_server.close()
486 if __name__ == '__main__':
490 # Let system.exit() calls complete normally
493 vlog.exception("traceback")
494 sys.exit(ovs.daemon.RESTART_EXIT_CODE)