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.
30 import logging, logging.handlers
36 from ovs.db import error
37 from ovs.db import types
42 s_log = logging.getLogger("ovs-monitor-ipsec")
44 # By default log messages as DAEMON into syslog
45 l_handler = logging.handlers.SysLogHandler(
47 facility=logging.handlers.SysLogHandler.LOG_DAEMON)
48 l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s')
49 l_handler.setFormatter(l_formatter)
50 s_log.addHandler(l_handler)
51 except socket.error, e:
53 s_log.warn("failed to connect to syslog (%s)" % e)
55 setkey = "/usr/sbin/setkey"
57 # Class to configure the racoon daemon, which handles IKE negotiation
59 # Default locations for files
60 conf_file = "/etc/racoon/racoon.conf"
61 cert_dir = "/etc/racoon/certs"
62 psk_file = "/etc/racoon/psk.txt"
64 # Racoon configuration header we use for IKE
65 conf_header = """# Configuration file generated by Open vSwitch
67 # Do not modify by hand!
69 path pre_shared_key "%s";
70 path certificate "%s";
74 # Racoon configuration footer we use for IKE
75 conf_footer = """sainfo anonymous {
78 encryption_algorithm aes;
79 authentication_algorithm hmac_sha1, hmac_md5;
80 compression_algorithm deflate;
85 # Certificate entry template.
86 cert_entry = """remote %s {
90 certificate_type x509 "%s" "%s";
92 peers_identifier asn1dn;
93 peers_certfile x509 "%s";
96 encryption_algorithm aes;
98 authentication_method rsasig;
105 # Pre-shared key template.
106 psk_entry = """remote %s {
110 encryption_algorithm aes;
112 authentication_method pre_shared_key;
123 if not os.path.isdir(self.cert_dir):
124 os.mkdir(self.cert_dir)
126 # Clean out stale peer certs from previous runs
127 for ovs_cert in glob.glob("%s/ovs-*.pem" % self.cert_dir):
131 s_log.warning("couldn't remove %s" % ovs_cert)
133 # Replace racoon's conf file with our template
137 exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
139 # Racoon is finicky about it's configuration file and will
140 # refuse to start if it sees something it doesn't like
141 # (e.g., a certificate file doesn't exist). Try restarting
142 # the process before giving up.
143 s_log.warning("attempting to restart racoon")
144 exitcode = subprocess.call(["/etc/init.d/racoon", "restart"])
146 s_log.warning("couldn't reload racoon")
149 # Rewrite the Racoon configuration file
150 conf_file = open(self.conf_file, 'w')
151 conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
153 for host, vals in self.cert_hosts.iteritems():
154 conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
155 vals["private_key"], vals["peer_cert_file"]))
157 for host in self.psk_hosts:
158 conf_file.write(Racoon.psk_entry % host)
160 conf_file.write(Racoon.conf_footer)
163 # Rewrite the pre-shared keys file; it must only be readable by root.
164 orig_umask = os.umask(0077)
165 psk_file = open(Racoon.psk_file, 'w')
168 psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
169 psk_file.write("\n\n")
170 for host, vals in self.psk_hosts.iteritems():
171 psk_file.write("%s %s\n" % (host, vals["psk"]))
176 def _add_psk(self, host, psk):
177 if host in self.cert_hosts:
178 raise error.Error("host %s already defined for cert" % host)
180 self.psk_hosts[host] = psk
183 def _verify_certs(self, vals):
184 # Racoon will refuse to start if the certificate files don't
185 # exist, so verify that they're there.
186 if not os.path.isfile(vals["certificate"]):
187 raise error.Error("'certificate' file does not exist: %s"
188 % vals["certificate"])
189 elif not os.path.isfile(vals["private_key"]):
190 raise error.Error("'private_key' file does not exist: %s"
191 % vals["private_key"])
193 # Racoon won't start if a given certificate or private key isn't
194 # valid. This is a weak test, but will detect the most flagrant
196 if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
197 raise error.Error("'peer_cert' is not in valid PEM format")
199 cert = open(vals["certificate"]).read()
200 if cert.find("-----BEGIN CERTIFICATE-----") == -1:
201 raise error.Error("'certificate' is not in valid PEM format")
203 cert = open(vals["private_key"]).read()
204 if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
205 raise error.Error("'private_key' is not in valid PEM format")
208 def _add_cert(self, host, vals):
209 if host in self.psk_hosts:
210 raise error.Error("host %s already defined for psk" % host)
212 if vals["certificate"] == None:
213 raise error.Error("'certificate' not defined for %s" % host)
214 elif vals["private_key"] == None:
215 # Assume the private key is stored in the same PEM file as
216 # the certificate. We make a copy of "vals" so that we don't
217 # modify the original "vals", which would cause the script
218 # to constantly think that the configuration has changed
221 vals["private_key"] = vals["certificate"]
223 self._verify_certs(vals)
225 # The peer's certificate comes to us in PEM format as a string.
226 # Write that string to a file for Racoon to use.
227 peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
228 f = open(peer_cert_file, "w")
229 f.write(vals["peer_cert"])
232 vals["peer_cert_file"] = peer_cert_file
234 self.cert_hosts[host] = vals
237 def _del_cert(self, host):
238 peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
239 del self.cert_hosts[host]
242 os.remove(peer_cert_file)
246 def add_entry(self, host, vals):
247 if vals["peer_cert"]:
248 self._add_cert(host, vals)
250 self._add_psk(host, vals)
252 def del_entry(self, host):
253 if host in self.cert_hosts:
255 elif host in self.psk_hosts:
256 del self.psk_hosts[host]
260 # Class to configure IPsec on a system using racoon for IKE and setkey
261 # for maintaining the Security Association Database (SAD) and Security
262 # Policy Database (SPD). Only policies for GRE are supported.
267 self.racoon = Racoon()
270 def call_setkey(self, cmds):
272 p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE,
273 stdout=subprocess.PIPE)
275 s_log.error("could not call setkey")
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.
282 return p.communicate()[0]
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.
290 host_line = "%s %s" % (local_ip, remote_ip)
291 results = self.call_setkey("dump ;").split("\n")
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
295 spi_line = results[i+1]
296 if (spi_line[1:4] == proto):
297 spi = spi_line.split()[2]
298 spi_list.append(spi.split('(')[1].rstrip(')'))
302 self.call_setkey("flush;")
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.
310 # Delete local_ip->remote_ip SAD entries
311 spi_list = self.get_spi(local_ip, remote_ip)
313 cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
315 # Delete remote_ip->local_ip SAD entries
316 spi_list = self.get_spi(remote_ip, local_ip)
318 cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
321 self.call_setkey(cmds)
324 self.call_setkey("spdflush;")
326 def spd_add(self, local_ip, remote_ip):
327 cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
328 (local_ip, remote_ip))
329 cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;" %
330 (remote_ip, local_ip))
331 self.call_setkey(cmds)
333 def spd_del(self, local_ip, remote_ip):
334 cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
335 cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
336 self.call_setkey(cmds)
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"
343 self.racoon.add_entry(remote_ip, vals)
344 self.spd_add(local_ip, remote_ip)
346 self.entries.append(remote_ip)
349 def del_entry(self, local_ip, remote_ip):
350 if remote_ip in self.entries:
351 self.racoon.del_entry(remote_ip)
352 self.spd_del(local_ip, remote_ip)
353 self.sad_del(local_ip, remote_ip)
355 self.entries.remove(remote_ip)
358 def keep_table_columns(schema, table_name, column_types):
359 table = schema.tables.get(table_name)
361 raise error.Error("schema has no %s table" % table_name)
364 for column_name, column_type in column_types.iteritems():
365 column = table.columns.get(column_name)
367 raise error.Error("%s table schema lacks %s column"
368 % (table_name, column_name))
369 if column.type != column_type:
370 raise error.Error("%s column in %s table has type \"%s\", "
371 "expected type \"%s\""
372 % (column_name, table_name,
373 column.type.toEnglish(),
374 column_type.toEnglish()))
375 new_columns[column_name] = column
376 table.columns = new_columns
379 def monitor_uuid_schema_cb(schema):
380 string_type = types.Type(types.BaseType(types.StringType))
381 optional_ssl_type = types.Type(types.BaseType(types.UuidType,
382 ref_table='SSL'), None, 0, 1)
383 string_map_type = types.Type(types.BaseType(types.StringType),
384 types.BaseType(types.StringType),
388 new_tables["Interface"] = keep_table_columns(
389 schema, "Interface", {"name": string_type,
391 "options": string_map_type})
392 new_tables["Open_vSwitch"] = keep_table_columns(
393 schema, "Open_vSwitch", {"ssl": optional_ssl_type})
394 new_tables["SSL"] = keep_table_columns(
395 schema, "SSL", {"certificate": string_type,
396 "private_key": string_type})
397 schema.tables = new_tables
400 print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
401 print "where DATABASE is a socket on which ovsdb-server is listening."
403 print "Other options:"
404 print " -h, --help display this help message"
407 def update_ipsec(ipsec, interfaces, new_interfaces):
408 for name, vals in interfaces.iteritems():
409 if name not in new_interfaces:
410 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
412 for name, vals in new_interfaces.iteritems():
413 orig_vals = interfaces.get(name)
415 # Configuration for this host already exists. Check if it's
417 if vals == orig_vals:
420 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
423 ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
424 except error.Error, msg:
425 s_log.warning("skipping ipsec config for %s: %s" % (name, msg))
427 def get_ssl_cert(data):
428 for ovs_rec in data["Open_vSwitch"].itervalues():
429 if ovs_rec.ssl.as_list():
430 ssl_rec = data["SSL"][ovs_rec.ssl.as_scalar()]
431 return (ssl_rec.certificate.as_scalar(),
432 ssl_rec.private_key.as_scalar())
438 options, args = getopt.gnu_getopt(
439 argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS)
440 except getopt.GetoptError, geo:
441 sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg))
444 for key, value in options:
445 if key in ['-h', '--help']:
447 elif not ovs.daemon.parse_opt(key, value):
448 sys.stderr.write("%s: unhandled option %s\n"
449 % (ovs.util.PROGRAM_NAME, key))
453 sys.stderr.write("%s: exactly one nonoption argument is required "
454 "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
458 idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
460 ovs.daemon.daemonize()
467 poller = ovs.poller.Poller()
472 ssl_cert = get_ssl_cert(idl.data)
475 for rec in idl.data["Interface"].itervalues():
476 if rec.type.as_scalar() == "ipsec_gre":
477 name = rec.name.as_scalar()
479 "remote_ip": rec.options.get("remote_ip"),
480 "local_ip": rec.options.get("local_ip", "0.0.0.0/0"),
481 "certificate": rec.options.get("certificate"),
482 "private_key": rec.options.get("private_key"),
483 "use_ssl_cert": rec.options.get("use_ssl_cert"),
484 "peer_cert": rec.options.get("peer_cert"),
485 "psk": rec.options.get("psk") }
487 if entry["peer_cert"] and entry["psk"]:
488 s_log.warning("both 'peer_cert' and 'psk' defined for %s"
491 elif not entry["peer_cert"] and not entry["psk"]:
492 s_log.warning("no 'peer_cert' or 'psk' defined for %s"
496 # The "use_ssl_cert" option is deprecated and will
497 # likely go away in the near future.
498 if entry["use_ssl_cert"] == "true":
500 s_log.warning("no valid SSL entry for %s" % name)
503 entry["certificate"] = ssl_cert[0]
504 entry["private_key"] = ssl_cert[1]
506 new_interfaces[name] = entry
508 if interfaces != new_interfaces:
509 update_ipsec(ipsec, interfaces, new_interfaces)
510 interfaces = new_interfaces
512 if __name__ == '__main__':
516 # Let system.exit() calls complete normally
519 s_log.exception("traceback")
520 sys.exit(ovs.daemon.RESTART_EXIT_CODE)