b9a41268c2e0eeaf5778b11e95c4068431d40a43
[sliver-openvswitch.git] / 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 socket
33 import subprocess
34 import sys
35
36 from ovs.db import error
37 from ovs.db import types
38 import ovs.util
39 import ovs.daemon
40 import ovs.db.idl
41
42 s_log = logging.getLogger("ovs-monitor-ipsec")
43 try:
44     # By default log messages as DAEMON into syslog
45     l_handler = logging.handlers.SysLogHandler(
46             "/dev/log",
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:
52     logging.basicConfig()
53     s_log.warn("failed to connect to syslog (%s)" % e)
54
55 setkey = "/usr/sbin/setkey"
56
57 # Class to configure the racoon daemon, which handles IKE negotiation
58 class Racoon:
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"
63
64     # Racoon configuration header we use for IKE
65     conf_header = """# Configuration file generated by Open vSwitch
66 #
67 # Do not modify by hand!
68
69 path pre_shared_key "%s";
70 path certificate "%s";
71
72 """
73
74     # Racoon configuration footer we use for IKE
75     conf_footer = """sainfo anonymous {
76         pfs_group 2;
77         lifetime time 1 hour;
78         encryption_algorithm aes;
79         authentication_algorithm hmac_sha1, hmac_md5;
80         compression_algorithm deflate;
81 }
82
83 """
84
85     # Certificate entry template.
86     cert_entry = """remote %s {
87         exchange_mode main;
88         nat_traversal on;
89         ike_frag on;
90         certificate_type x509 "%s" "%s";
91         my_identifier asn1dn;
92         peers_identifier asn1dn;
93         peers_certfile x509 "%s";
94         verify_identifier on;
95         proposal {
96                 encryption_algorithm aes;
97                 hash_algorithm sha1;
98                 authentication_method rsasig;
99                 dh_group 2;
100         }
101 }
102
103 """
104
105     # Pre-shared key template.
106     psk_entry = """remote %s {
107         exchange_mode main;
108         nat_traversal on;
109         proposal {
110                 encryption_algorithm aes;
111                 hash_algorithm sha1;
112                 authentication_method pre_shared_key;
113                 dh_group 2;
114         }
115 }
116
117 """
118
119     def __init__(self):
120         self.psk_hosts = {}
121         self.cert_hosts = {}
122
123         if not os.path.isdir(self.cert_dir):
124             os.mkdir(self.cert_dir)
125
126         # Clean out stale peer certs from previous runs
127         for ovs_cert in glob.glob("%s/ovs-*.pem" % self.cert_dir):
128             try:
129                 os.remove(ovs_cert)
130             except OSError:
131                 s_log.warning("couldn't remove %s" % ovs_cert)
132
133         # Replace racoon's conf file with our template
134         self.commit()
135
136     def reload(self):
137         exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
138         if exitcode != 0:
139             # Racoon is finicky about its 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"])
145             if exitcode != 0:
146                 s_log.warning("couldn't reload racoon")
147
148     def commit(self):
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))
152
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"]))
156
157         for host in self.psk_hosts:
158             conf_file.write(Racoon.psk_entry % host)
159
160         conf_file.write(Racoon.conf_footer)
161         conf_file.close()
162
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')
166         os.umask(orig_umask)
167
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"]))
172         psk_file.close()
173
174         self.reload()
175
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)
179
180         self.psk_hosts[host] = psk
181         self.commit()
182
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"])
192
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
195         # errors.
196         if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
197             raise error.Error("'peer_cert' is not in valid PEM format")
198
199         cert = open(vals["certificate"]).read()
200         if cert.find("-----BEGIN CERTIFICATE-----") == -1:
201             raise error.Error("'certificate' is not in valid PEM format")
202
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")
206             
207
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)
211
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
219             # in the database.
220             vals = vals.copy()
221             vals["private_key"] = vals["certificate"]
222
223         self._verify_certs(vals)
224
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"])
230         f.close()
231
232         vals["peer_cert_file"] = peer_cert_file
233
234         self.cert_hosts[host] = vals
235         self.commit()
236
237     def _del_cert(self, host):
238         peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
239         del self.cert_hosts[host]
240         self.commit()
241         try:
242             os.remove(peer_cert_file)
243         except OSError:
244             pass
245
246     def add_entry(self, host, vals):
247         if vals["peer_cert"]:
248             self._add_cert(host, vals)
249         elif vals["psk"]:
250             self._add_psk(host, vals)
251
252     def del_entry(self, host):
253         if host in self.cert_hosts:
254             self._del_cert(host)
255         elif host in self.psk_hosts:
256             del self.psk_hosts[host]
257             self.commit()
258
259
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.
263 class IPsec:
264     def __init__(self):
265         self.sad_flush()
266         self.spd_flush()
267         self.racoon = Racoon()
268         self.entries = []
269
270     def call_setkey(self, cmds):
271         try:
272             p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, 
273                     stdout=subprocess.PIPE)
274         except:
275             s_log.error("could not call setkey")
276             sys.exit(1)
277
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.
281         p.stdin.write(cmds)
282         return p.communicate()[0]
283
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.
289         spi_list = []
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(')'))
299         return spi_list
300
301     def sad_flush(self):
302         self.call_setkey("flush;")
303
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.
308         cmds = ""
309
310         # Delete local_ip->remote_ip SAD entries
311         spi_list = self.get_spi(local_ip, remote_ip)
312         for spi in spi_list:
313             cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
314
315         # Delete remote_ip->local_ip SAD entries
316         spi_list = self.get_spi(remote_ip, local_ip)
317         for spi in spi_list:
318             cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
319
320         if cmds:
321             self.call_setkey(cmds)
322
323     def spd_flush(self):
324         self.call_setkey("spdflush;")
325
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)
332
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)
337
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"
341                               % remote_ip)
342
343         self.racoon.add_entry(remote_ip, vals)
344         self.spd_add(local_ip, remote_ip)
345
346         self.entries.append(remote_ip)
347
348
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)
354
355             self.entries.remove(remote_ip)
356
357
358 def keep_table_columns(schema, table_name, column_types):
359     table = schema.tables.get(table_name)
360     if not table:
361         raise error.Error("schema has no %s table" % table_name)
362
363     new_columns = {}
364     for column_name, column_type in column_types.iteritems():
365         column = table.columns.get(column_name)
366         if not column:
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
377     return table
378  
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_name='SSL'), None, 0, 1)
383     string_map_type = types.Type(types.BaseType(types.StringType),
384                                  types.BaseType(types.StringType),
385                                  0, sys.maxint)
386  
387     new_tables = {}
388     new_tables["Interface"] = keep_table_columns(
389         schema, "Interface", {"name": string_type,
390                               "type": 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
398
399 def usage():
400     print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
401     print "where DATABASE is a socket on which ovsdb-server is listening."
402     ovs.daemon.usage()
403     print "Other options:"
404     print "  -h, --help               display this help message"
405     sys.exit(0)
406  
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"])
411
412     for name, vals in new_interfaces.iteritems():
413         orig_vals = interfaces.get(name)
414         if orig_vals:
415             # Configuration for this host already exists.  Check if it's
416             # changed.
417             if vals == orig_vals:
418                 continue
419             else:
420                 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
421
422         try:
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))
426
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())
433
434     return None
435
436 def main(argv):
437     try:
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))
442         sys.exit(1)
443  
444     for key, value in options:
445         if key in ['-h', '--help']:
446             usage()
447         elif not ovs.daemon.parse_opt(key, value):
448             sys.stderr.write("%s: unhandled option %s\n"
449                              % (ovs.util.PROGRAM_NAME, key))
450             sys.exit(1)
451  
452     if len(args) != 1:
453         sys.stderr.write("%s: exactly one nonoption argument is required "
454                          "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
455         sys.exit(1)
456
457     remote = args[0]
458     idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
459
460     ovs.daemon.daemonize()
461
462     ipsec = IPsec()
463
464     interfaces = {}
465     while True:
466         if not idl.run():
467             poller = ovs.poller.Poller()
468             idl.wait(poller)
469             poller.block()
470             continue
471
472         ssl_cert = get_ssl_cert(idl.data)
473  
474         new_interfaces = {}
475         for rec in idl.data["Interface"].itervalues():
476             if rec.type.as_scalar() == "ipsec_gre":
477                 name = rec.name.as_scalar()
478                 entry = {
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") }
486
487                 if entry["peer_cert"] and entry["psk"]:
488                     s_log.warning("both 'peer_cert' and 'psk' defined for %s" 
489                             % name)
490                     continue
491                 elif not entry["peer_cert"] and not entry["psk"]:
492                     s_log.warning("no 'peer_cert' or 'psk' defined for %s" 
493                             % name)
494                     continue
495
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":
499                     if not ssl_cert:
500                         s_log.warning("no valid SSL entry for %s" % name)
501                         continue
502
503                     entry["certificate"] = ssl_cert[0]
504                     entry["private_key"] = ssl_cert[1]
505
506                 new_interfaces[name] = entry
507  
508         if interfaces != new_interfaces:
509             update_ipsec(ipsec, interfaces, new_interfaces)
510             interfaces = new_interfaces
511  
512 if __name__ == '__main__':
513     try:
514         main(sys.argv)
515     except SystemExit:
516         # Let system.exit() calls complete normally
517         raise
518     except:
519         s_log.exception("traceback")
520         sys.exit(ovs.daemon.RESTART_EXIT_CODE)