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