2 # Copyright (c) 2009, 2010, 2011 Nicira Networks
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.
36 from ovs.db import error
37 from ovs.db import types
43 vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
44 root_prefix = '' # Prefix for absolute file names, for testing.
45 setkey = "/usr/sbin/setkey"
48 # Class to configure the racoon daemon, which handles IKE negotiation
50 # Default locations for files
51 conf_file = "/etc/racoon/racoon.conf"
52 cert_dir = "/etc/racoon/certs"
53 psk_file = "/etc/racoon/psk.txt"
55 # Racoon configuration header we use for IKE
56 conf_header = """# Configuration file generated by Open vSwitch
58 # Do not modify by hand!
60 path pre_shared_key "%s";
61 path certificate "%s";
65 # Racoon configuration footer we use for IKE
66 conf_footer = """sainfo anonymous {
69 encryption_algorithm aes;
70 authentication_algorithm hmac_sha1, hmac_md5;
71 compression_algorithm deflate;
76 # Certificate entry template.
77 cert_entry = """remote %s {
81 certificate_type x509 "%s" "%s";
83 peers_identifier asn1dn;
84 peers_certfile x509 "%s";
87 encryption_algorithm aes;
89 authentication_method rsasig;
96 # Pre-shared key template.
97 psk_entry = """remote %s {
101 encryption_algorithm aes;
103 authentication_method pre_shared_key;
114 if not os.path.isdir(root_prefix + self.cert_dir):
115 os.mkdir(self.cert_dir)
117 # Clean out stale peer certs from previous runs
118 for ovs_cert in glob.glob("%s%s/ovs-*.pem"
119 % (root_prefix, self.cert_dir)):
123 s_log.warning("couldn't remove %s" % ovs_cert)
125 # Replace racoon's conf file with our template
129 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
132 # Racoon is finicky about its configuration file and will
133 # refuse to start if it sees something it doesn't like
134 # (e.g., a certificate file doesn't exist). Try restarting
135 # the process before giving up.
136 s_log.warning("attempting to restart racoon")
137 exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
140 s_log.warning("couldn't reload racoon")
143 # Rewrite the Racoon configuration file
144 conf_file = open(root_prefix + self.conf_file, 'w')
145 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
147 for host, vals in self.cert_hosts.iteritems():
148 conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
149 vals["private_key"], vals["peer_cert_file"]))
151 for host in self.psk_hosts:
152 conf_file.write(Racoon.psk_entry % host)
154 conf_file.write(Racoon.conf_footer)
157 # Rewrite the pre-shared keys file; it must only be readable by root.
158 orig_umask = os.umask(0077)
159 psk_file = open(root_prefix + Racoon.psk_file, 'w')
162 psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
163 psk_file.write("\n\n")
164 for host, vals in self.psk_hosts.iteritems():
165 psk_file.write("%s %s\n" % (host, vals["psk"]))
170 def _add_psk(self, host, psk):
171 if host in self.cert_hosts:
172 raise error.Error("host %s already defined for cert" % host)
174 self.psk_hosts[host] = psk
177 def _verify_certs(self, vals):
178 # Racoon will refuse to start if the certificate files don't
179 # exist, so verify that they're there.
180 if not os.path.isfile(root_prefix + vals["certificate"]):
181 raise error.Error("'certificate' file does not exist: %s"
182 % vals["certificate"])
183 elif not os.path.isfile(root_prefix + vals["private_key"]):
184 raise error.Error("'private_key' file does not exist: %s"
185 % vals["private_key"])
187 # Racoon won't start if a given certificate or private key isn't
188 # valid. This is a weak test, but will detect the most flagrant
190 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
191 raise error.Error("'peer_cert' is not in valid PEM format")
193 cert = open(root_prefix + vals["certificate"]).read()
194 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
195 raise error.Error("'certificate' is not in valid PEM format")
197 cert = open(root_prefix + vals["private_key"]).read()
198 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
199 raise error.Error("'private_key' is not in valid PEM format")
201 def _add_cert(self, host, vals):
202 if host in self.psk_hosts:
203 raise error.Error("host %s already defined for psk" % host)
205 if vals["certificate"] == None:
206 raise error.Error("'certificate' not defined for %s" % host)
207 elif vals["private_key"] == None:
208 # Assume the private key is stored in the same PEM file as
209 # the certificate. We make a copy of "vals" so that we don't
210 # modify the original "vals", which would cause the script
211 # to constantly think that the configuration has changed
214 vals["private_key"] = vals["certificate"]
216 self._verify_certs(vals)
218 # The peer's certificate comes to us in PEM format as a string.
219 # Write that string to a file for Racoon to use.
220 peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
221 f = open(root_prefix + peer_cert_file, "w")
222 f.write(vals["peer_cert"])
225 vals["peer_cert_file"] = peer_cert_file
227 self.cert_hosts[host] = vals
230 def _del_cert(self, host):
231 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
232 del self.cert_hosts[host]
235 os.remove(root_prefix + peer_cert_file)
239 def add_entry(self, host, vals):
240 if vals["peer_cert"]:
241 self._add_cert(host, vals)
243 self._add_psk(host, vals)
245 def del_entry(self, host):
246 if host in self.cert_hosts:
248 elif host in self.psk_hosts:
249 del self.psk_hosts[host]
253 # Class to configure IPsec on a system using racoon for IKE and setkey
254 # for maintaining the Security Association Database (SAD) and Security
255 # Policy Database (SPD). Only policies for GRE are supported.
260 self.racoon = Racoon()
263 def call_setkey(self, cmds):
265 p = subprocess.Popen([root_prefix + setkey, "-c"],
266 stdin=subprocess.PIPE,
267 stdout=subprocess.PIPE)
269 s_log.error("could not call %s%s" % (root_prefix, setkey))
272 # xxx It is safer to pass the string into the communicate()
273 # xxx method, but it didn't work for slightly longer commands.
274 # xxx An alternative may need to be found.
276 return p.communicate()[0]
278 def get_spi(self, local_ip, remote_ip, proto="esp"):
279 # Run the setkey dump command to retrieve the SAD. Then, parse
280 # the output looking for SPI buried in the output. Note that
281 # multiple SAD entries can exist for the same "flow", since an
282 # older entry could be in a "dying" state.
284 host_line = "%s %s" % (local_ip, remote_ip)
285 results = self.call_setkey("dump ;\n").split("\n")
286 for i in range(len(results)):
287 if results[i].strip() == host_line:
288 # The SPI is in the line following the host pair
289 spi_line = results[i + 1]
290 if (spi_line[1:4] == proto):
291 spi = spi_line.split()[2]
292 spi_list.append(spi.split('(')[1].rstrip(')'))
296 self.call_setkey("flush;\n")
298 def sad_del(self, local_ip, remote_ip):
299 # To delete all SAD entries, we should be able to use setkey's
300 # "deleteall" command. Unfortunately, it's fundamentally broken
301 # on Linux and not documented as such.
304 # Delete local_ip->remote_ip SAD entries
305 spi_list = self.get_spi(local_ip, remote_ip)
307 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
309 # Delete remote_ip->local_ip SAD entries
310 spi_list = self.get_spi(remote_ip, local_ip)
312 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
315 self.call_setkey(cmds)
318 self.call_setkey("spdflush;\n")
320 def spd_add(self, local_ip, remote_ip):
321 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
322 (local_ip, remote_ip))
323 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
324 (remote_ip, local_ip))
325 self.call_setkey(cmds)
327 def spd_del(self, local_ip, remote_ip):
328 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
329 cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
330 self.call_setkey(cmds)
332 def add_entry(self, local_ip, remote_ip, vals):
333 if remote_ip in self.entries:
334 raise error.Error("host %s already configured for ipsec"
337 self.racoon.add_entry(remote_ip, vals)
338 self.spd_add(local_ip, remote_ip)
340 self.entries.append(remote_ip)
342 def del_entry(self, local_ip, remote_ip):
343 if remote_ip in self.entries:
344 self.racoon.del_entry(remote_ip)
345 self.spd_del(local_ip, remote_ip)
346 self.sad_del(local_ip, remote_ip)
348 self.entries.remove(remote_ip)
351 def keep_table_columns(schema, table_name, column_types):
352 table = schema.tables.get(table_name)
354 raise error.Error("schema has no %s table" % table_name)
357 for column_name, column_type in column_types.iteritems():
358 column = table.columns.get(column_name)
360 raise error.Error("%s table schema lacks %s column"
361 % (table_name, column_name))
362 if column.type != column_type:
363 raise error.Error("%s column in %s table has type \"%s\", "
364 "expected type \"%s\""
365 % (column_name, table_name,
366 column.type.toEnglish(),
367 column_type.toEnglish()))
368 new_columns[column_name] = column
369 table.columns = new_columns
373 def prune_schema(schema):
374 string_type = types.Type(types.BaseType(types.StringType))
375 optional_ssl_type = types.Type(types.BaseType(types.UuidType,
376 ref_table_name='SSL'), None, 0, 1)
377 string_map_type = types.Type(types.BaseType(types.StringType),
378 types.BaseType(types.StringType),
382 new_tables["Interface"] = keep_table_columns(
383 schema, "Interface", {"name": string_type,
385 "options": string_map_type})
386 new_tables["Open_vSwitch"] = keep_table_columns(
387 schema, "Open_vSwitch", {"ssl": optional_ssl_type})
388 new_tables["SSL"] = keep_table_columns(
389 schema, "SSL", {"certificate": string_type,
390 "private_key": string_type})
391 schema.tables = new_tables
394 def update_ipsec(ipsec, interfaces, new_interfaces):
395 for name, vals in interfaces.iteritems():
396 if name not in new_interfaces:
397 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
399 for name, vals in new_interfaces.iteritems():
400 orig_vals = interfaces.get(name)
402 # Configuration for this host already exists. Check if it's
404 if vals == orig_vals:
407 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
410 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
411 except error.Error, msg:
412 s_log.warning("skipping ipsec config for %s: %s" % (name, msg))
415 def get_ssl_cert(data):
416 for ovs_rec in data["Open_vSwitch"].rows.itervalues():
418 if ssl and ssl.certificate and ssl.private_key:
419 return (ssl.certificate, ssl.private_key)
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"
433 ovs.vlog.add_args(parser)
434 ovs.daemon.add_args(parser)
435 args = parser.parse_args()
436 ovs.vlog.handle_args(args)
437 ovs.daemon.handle_args(args)
440 root_prefix = args.root_prefix
442 remote = args.database
443 schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
444 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
446 idl = ovs.db.idl.Idl(remote, schema)
448 ovs.daemon.daemonize()
455 poller = ovs.poller.Poller()
460 ssl_cert = get_ssl_cert(idl.tables)
463 for rec in idl.tables["Interface"].rows.itervalues():
464 if rec.type == "ipsec_gre":
466 options = rec.options
468 "remote_ip": options.get("remote_ip"),
469 "local_ip": options.get("local_ip", "0.0.0.0/0"),
470 "certificate": options.get("certificate"),
471 "private_key": options.get("private_key"),
472 "use_ssl_cert": options.get("use_ssl_cert"),
473 "peer_cert": options.get("peer_cert"),
474 "psk": options.get("psk")}
476 if entry["peer_cert"] and entry["psk"]:
477 s_log.warning("both 'peer_cert' and 'psk' defined for %s"
480 elif not entry["peer_cert"] and not entry["psk"]:
481 s_log.warning("no 'peer_cert' or 'psk' defined for %s"
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":
489 s_log.warning("no valid SSL entry for %s" % name)
492 entry["certificate"] = ssl_cert[0]
493 entry["private_key"] = ssl_cert[1]
495 new_interfaces[name] = entry
497 if interfaces != new_interfaces:
498 update_ipsec(ipsec, interfaces, new_interfaces)
499 interfaces = new_interfaces
502 if __name__ == '__main__':
506 # Let system.exit() calls complete normally
509 vlog.exception("traceback")
510 sys.exit(ovs.daemon.RESTART_EXIT_CODE)