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