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