vlog: Output configuration list in alphabetical order.
[openvswitch] / debian / ovs-monitor-ipsec
1 #!/usr/bin/python
2 # Copyright (c) 2009, 2010, 2011 Nicira Networks
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
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.
26
27
28 import getopt
29 import glob
30 import logging, logging.handlers
31 import os
32 import subprocess
33 import sys
34
35 from ovs.db import error
36 from ovs.db import types
37 import ovs.util
38 import ovs.daemon
39 import ovs.db.idl
40
41
42 # By default log messages as DAEMON into syslog
43 s_log = logging.getLogger("ovs-monitor-ipsec")
44 l_handler = logging.handlers.SysLogHandler(
45         "/dev/log",
46         facility=logging.handlers.SysLogHandler.LOG_DAEMON)
47 l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s')
48 l_handler.setFormatter(l_formatter)
49 s_log.addHandler(l_handler)
50
51
52 setkey = "/usr/sbin/setkey"
53
54 # Class to configure the racoon daemon, which handles IKE negotiation
55 class Racoon:
56     # Default locations for files
57     conf_file = "/etc/racoon/racoon.conf"
58     cert_dir = "/etc/racoon/certs"
59     psk_file = "/etc/racoon/psk.txt"
60
61     # Racoon configuration header we use for IKE
62     conf_header = """# Configuration file generated by Open vSwitch
63 #
64 # Do not modify by hand!
65
66 path pre_shared_key "%s";
67 path certificate "%s";
68
69 """
70
71     # Racoon configuration footer we use for IKE
72     conf_footer = """sainfo anonymous {
73         pfs_group 2;
74         lifetime time 1 hour;
75         encryption_algorithm aes;
76         authentication_algorithm hmac_sha1, hmac_md5;
77         compression_algorithm deflate;
78 }
79
80 """
81
82     # Certificate entry template.
83     cert_entry = """remote %s {
84         exchange_mode main;
85         nat_traversal on;
86         ike_frag on;
87         certificate_type x509 "%s" "%s";
88         my_identifier asn1dn;
89         peers_identifier asn1dn;
90         peers_certfile x509 "%s";
91         verify_identifier on;
92         proposal {
93                 encryption_algorithm aes;
94                 hash_algorithm sha1;
95                 authentication_method rsasig;
96                 dh_group 2;
97         }
98 }
99
100 """
101
102     # Pre-shared key template.
103     psk_entry = """remote %s {
104         exchange_mode main;
105         nat_traversal on;
106         proposal {
107                 encryption_algorithm aes;
108                 hash_algorithm sha1;
109                 authentication_method pre_shared_key;
110                 dh_group 2;
111         }
112 }
113
114 """
115
116     def __init__(self):
117         self.psk_hosts = {}
118         self.cert_hosts = {}
119
120         if not os.path.isdir(self.cert_dir):
121             os.mkdir(self.cert_dir)
122
123         # Clean out stale peer certs from previous runs
124         for ovs_cert in glob.glob("%s/ovs-*.pem" % self.cert_dir):
125             try:
126                 os.remove(ovs_cert)
127             except OSError:
128                 s_log.warning("couldn't remove %s" % ovs_cert)
129
130         # Replace racoon's conf file with our template
131         self.commit()
132
133     def reload(self):
134         exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
135         if exitcode != 0:
136             # Racoon is finicky about it's configuration file and will
137             # refuse to start if it sees something it doesn't like
138             # (e.g., a certificate file doesn't exist).  Try restarting
139             # the process before giving up.
140             s_log.warning("attempting to restart racoon")
141             exitcode = subprocess.call(["/etc/init.d/racoon", "restart"])
142             if exitcode != 0:
143                 s_log.warning("couldn't reload racoon")
144
145     def commit(self):
146         # Rewrite the Racoon configuration file
147         conf_file = open(self.conf_file, 'w')
148         conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir))
149
150         for host, vals in self.cert_hosts.iteritems():
151             conf_file.write(Racoon.cert_entry % (host, vals["certificate"],
152                     vals["private_key"], vals["peer_cert_file"]))
153
154         for host in self.psk_hosts:
155             conf_file.write(Racoon.psk_entry % host)
156
157         conf_file.write(Racoon.conf_footer)
158         conf_file.close()
159
160         # Rewrite the pre-shared keys file; it must only be readable by root.
161         orig_umask = os.umask(0077)
162         psk_file = open(Racoon.psk_file, 'w')
163         os.umask(orig_umask)
164
165         psk_file.write("# Generated by Open vSwitch...do not modify by hand!")
166         psk_file.write("\n\n")
167         for host, vals in self.psk_hosts.iteritems():
168             psk_file.write("%s   %s\n" % (host, vals["psk"]))
169         psk_file.close()
170
171         self.reload()
172
173     def _add_psk(self, host, psk):
174         if host in self.cert_hosts:
175             raise error.Error("host %s already defined for cert" % host)
176
177         self.psk_hosts[host] = psk
178         self.commit()
179
180     def _verify_certs(self, vals):
181         # Racoon will refuse to start if the certificate files don't
182         # exist, so verify that they're there.
183         if not os.path.isfile(vals["certificate"]):
184             raise error.Error("'certificate' file does not exist: %s"
185                     % vals["certificate"])
186         elif not os.path.isfile(vals["private_key"]):
187             raise error.Error("'private_key' file does not exist: %s"
188                     % vals["private_key"])
189
190         # Racoon won't start if a given certificate or private key isn't
191         # valid.  This is a weak test, but will detect the most flagrant
192         # errors.
193         if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
194             raise error.Error("'peer_cert' is not in valid PEM format")
195
196         cert = open(vals["certificate"]).read()
197         if cert.find("-----BEGIN CERTIFICATE-----") == -1:
198             raise error.Error("'certificate' is not in valid PEM format")
199
200         cert = open(vals["private_key"]).read()
201         if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
202             raise error.Error("'private_key' is not in valid PEM format")
203             
204
205     def _add_cert(self, host, vals):
206         if host in self.psk_hosts:
207             raise error.Error("host %s already defined for psk" % host)
208
209         if vals["certificate"] == None:
210             raise error.Error("'certificate' not defined for %s" % host)
211         elif vals["private_key"] == None:
212             # Assume the private key is stored in the same PEM file as 
213             # the certificate.  We make a copy of "vals" so that we don't
214             # modify the original "vals", which would cause the script
215             # to constantly think that the configuration has changed
216             # in the database.
217             vals = vals.copy()
218             vals["private_key"] = vals["certificate"]
219
220         self._verify_certs(vals)
221
222         # The peer's certificate comes to us in PEM format as a string.
223         # Write that string to a file for Racoon to use.
224         peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
225         f = open(peer_cert_file, "w")
226         f.write(vals["peer_cert"])
227         f.close()
228
229         vals["peer_cert_file"] = peer_cert_file
230
231         self.cert_hosts[host] = vals
232         self.commit()
233
234     def _del_cert(self, host):
235         peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
236         del self.cert_hosts[host]
237         self.commit()
238         try:
239             os.remove(peer_cert_file)
240         except OSError:
241             pass
242
243     def add_entry(self, host, vals):
244         if vals["peer_cert"]:
245             self._add_cert(host, vals)
246         elif vals["psk"]:
247             self._add_psk(host, vals)
248
249     def del_entry(self, host):
250         if host in self.cert_hosts:
251             self._del_cert(host)
252         elif host in self.psk_hosts:
253             del self.psk_hosts[host]
254             self.commit()
255
256
257 # Class to configure IPsec on a system using racoon for IKE and setkey
258 # for maintaining the Security Association Database (SAD) and Security
259 # Policy Database (SPD).  Only policies for GRE are supported.
260 class IPsec:
261     def __init__(self):
262         self.sad_flush()
263         self.spd_flush()
264         self.racoon = Racoon()
265         self.entries = []
266
267     def call_setkey(self, cmds):
268         try:
269             p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, 
270                     stdout=subprocess.PIPE)
271         except:
272             s_log.error("could not call setkey")
273             sys.exit(1)
274
275         # xxx It is safer to pass the string into the communicate()
276         # xxx method, but it didn't work for slightly longer commands.
277         # xxx An alternative may need to be found.
278         p.stdin.write(cmds)
279         return p.communicate()[0]
280
281     def get_spi(self, local_ip, remote_ip, proto="esp"):
282         # Run the setkey dump command to retrieve the SAD.  Then, parse
283         # the output looking for SPI buried in the output.  Note that
284         # multiple SAD entries can exist for the same "flow", since an
285         # older entry could be in a "dying" state.
286         spi_list = []
287         host_line = "%s %s" % (local_ip, remote_ip)
288         results = self.call_setkey("dump ;").split("\n")
289         for i in range(len(results)):
290             if results[i].strip() == host_line:
291                 # The SPI is in the line following the host pair
292                 spi_line = results[i+1]
293                 if (spi_line[1:4] == proto):
294                     spi = spi_line.split()[2]
295                     spi_list.append(spi.split('(')[1].rstrip(')'))
296         return spi_list
297
298     def sad_flush(self):
299         self.call_setkey("flush;")
300
301     def sad_del(self, local_ip, remote_ip):
302         # To delete all SAD entries, we should be able to use setkey's
303         # "deleteall" command.  Unfortunately, it's fundamentally broken
304         # on Linux and not documented as such.
305         cmds = ""
306
307         # Delete local_ip->remote_ip SAD entries
308         spi_list = self.get_spi(local_ip, remote_ip)
309         for spi in spi_list:
310             cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
311
312         # Delete remote_ip->local_ip SAD entries
313         spi_list = self.get_spi(remote_ip, local_ip)
314         for spi in spi_list:
315             cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
316
317         if cmds:
318             self.call_setkey(cmds)
319
320     def spd_flush(self):
321         self.call_setkey("spdflush;")
322
323     def spd_add(self, local_ip, remote_ip):
324         cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
325                     (local_ip, remote_ip))
326         cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;" %
327                     (remote_ip, local_ip))
328         self.call_setkey(cmds)
329
330     def spd_del(self, local_ip, remote_ip):
331         cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
332         cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
333         self.call_setkey(cmds)
334
335     def add_entry(self, local_ip, remote_ip, vals):
336         if remote_ip in self.entries:
337             raise error.Error("host %s already configured for ipsec"
338                               % remote_ip)
339
340         self.racoon.add_entry(remote_ip, vals)
341         self.spd_add(local_ip, remote_ip)
342
343         self.entries.append(remote_ip)
344
345
346     def del_entry(self, local_ip, remote_ip):
347         if remote_ip in self.entries:
348             self.racoon.del_entry(remote_ip)
349             self.spd_del(local_ip, remote_ip)
350             self.sad_del(local_ip, remote_ip)
351
352             self.entries.remove(remote_ip)
353
354
355 def keep_table_columns(schema, table_name, column_types):
356     table = schema.tables.get(table_name)
357     if not table:
358         raise error.Error("schema has no %s table" % table_name)
359
360     new_columns = {}
361     for column_name, column_type in column_types.iteritems():
362         column = table.columns.get(column_name)
363         if not column:
364             raise error.Error("%s table schema lacks %s column"
365                               % (table_name, column_name))
366         if column.type != column_type:
367             raise error.Error("%s column in %s table has type \"%s\", "
368                               "expected type \"%s\""
369                               % (column_name, table_name,
370                                  column.type.toEnglish(),
371                                  column_type.toEnglish()))
372         new_columns[column_name] = column
373     table.columns = new_columns
374     return table
375  
376 def monitor_uuid_schema_cb(schema):
377     string_type = types.Type(types.BaseType(types.StringType))
378     optional_ssl_type = types.Type(types.BaseType(types.UuidType,
379                                                   ref_table='SSL'), None, 0, 1)
380     string_map_type = types.Type(types.BaseType(types.StringType),
381                                  types.BaseType(types.StringType),
382                                  0, sys.maxint)
383  
384     new_tables = {}
385     new_tables["Interface"] = keep_table_columns(
386         schema, "Interface", {"name": string_type,
387                               "type": string_type,
388                               "options": string_map_type})
389     new_tables["Open_vSwitch"] = keep_table_columns(
390         schema, "Open_vSwitch", {"ssl": optional_ssl_type})
391     new_tables["SSL"] = keep_table_columns(
392         schema, "SSL", {"certificate": string_type,
393                         "private_key": string_type})
394     schema.tables = new_tables
395
396 def usage():
397     print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
398     print "where DATABASE is a socket on which ovsdb-server is listening."
399     ovs.daemon.usage()
400     print "Other options:"
401     print "  -h, --help               display this help message"
402     sys.exit(0)
403  
404 def update_ipsec(ipsec, interfaces, new_interfaces):
405     for name, vals in interfaces.iteritems():
406         if name not in new_interfaces:
407             ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
408
409     for name, vals in new_interfaces.iteritems():
410         orig_vals = interfaces.get(name)
411         if orig_vals:
412             # Configuration for this host already exists.  Check if it's
413             # changed.
414             if vals == orig_vals:
415                 continue
416             else:
417                 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
418
419         try:
420             ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
421         except error.Error, msg:
422             s_log.warning("skipping ipsec config for %s: %s" % (name, msg))
423
424 def get_ssl_cert(data):
425     for ovs_rec in data["Open_vSwitch"].itervalues():
426         if ovs_rec.ssl.as_list():
427             ssl_rec = data["SSL"][ovs_rec.ssl.as_scalar()]
428             return (ssl_rec.certificate.as_scalar(),
429                     ssl_rec.private_key.as_scalar())
430
431     return None
432
433 def main(argv):
434     try:
435         options, args = getopt.gnu_getopt(
436             argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS)
437     except getopt.GetoptError, geo:
438         sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg))
439         sys.exit(1)
440  
441     for key, value in options:
442         if key in ['-h', '--help']:
443             usage()
444         elif not ovs.daemon.parse_opt(key, value):
445             sys.stderr.write("%s: unhandled option %s\n"
446                              % (ovs.util.PROGRAM_NAME, key))
447             sys.exit(1)
448  
449     if len(args) != 1:
450         sys.stderr.write("%s: exactly one nonoption argument is required "
451                          "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
452         sys.exit(1)
453
454     remote = args[0]
455     idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
456
457     ovs.daemon.daemonize()
458
459     ipsec = IPsec()
460
461     interfaces = {}
462     while True:
463         if not idl.run():
464             poller = ovs.poller.Poller()
465             idl.wait(poller)
466             poller.block()
467             continue
468
469         ssl_cert = get_ssl_cert(idl.data)
470  
471         new_interfaces = {}
472         for rec in idl.data["Interface"].itervalues():
473             if rec.type.as_scalar() == "ipsec_gre":
474                 name = rec.name.as_scalar()
475                 entry = {
476                     "remote_ip": rec.options.get("remote_ip"),
477                     "local_ip": rec.options.get("local_ip", "0.0.0.0/0"),
478                     "certificate": rec.options.get("certificate"),
479                     "private_key": rec.options.get("private_key"),
480                     "use_ssl_cert": rec.options.get("use_ssl_cert"),
481                     "peer_cert": rec.options.get("peer_cert"),
482                     "psk": rec.options.get("psk") }
483
484                 if entry["peer_cert"] and entry["psk"]:
485                     s_log.warning("both 'peer_cert' and 'psk' defined for %s" 
486                             % name)
487                     continue
488                 elif not entry["peer_cert"] and not entry["psk"]:
489                     s_log.warning("no 'peer_cert' or 'psk' defined for %s" 
490                             % name)
491                     continue
492
493                 # The "use_ssl_cert" option is deprecated and will
494                 # likely go away in the near future.
495                 if entry["use_ssl_cert"] == "true":
496                     if not ssl_cert:
497                         s_log.warning("no valid SSL entry for %s" % name)
498                         continue
499
500                     entry["certificate"] = ssl_cert[0]
501                     entry["private_key"] = ssl_cert[1]
502
503                 new_interfaces[name] = entry
504  
505         if interfaces != new_interfaces:
506             update_ipsec(ipsec, interfaces, new_interfaces)
507             interfaces = new_interfaces
508  
509 if __name__ == '__main__':
510     try:
511         main(sys.argv)
512     except SystemExit:
513         # Let system.exit() calls complete normally
514         raise
515     except:
516         s_log.exception("traceback")
517         sys.exit(ovs.daemon.RESTART_EXIT_CODE)