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