python: Upgrade daemon module to argparse.
[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 argparse
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 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
429 def get_ssl_cert(data):
430     for ovs_rec in data["Open_vSwitch"].rows.itervalues():
431         ssl = ovs_rec.ssl
432         if ssl and ssl.certificate and ssl.private_key:
433             return (ssl.certificate, ssl.private_key)
434
435     return None
436
437
438 def main():
439
440     parser = argparse.ArgumentParser()
441     parser.add_argument("database", metavar="DATABASE",
442                         help="A socket on which ovsdb-server is listening.")
443     parser.add_argument("--root-prefix", metavar="DIR",
444                         help="Use DIR as alternate root directory"
445                         " (for testing).")
446
447     ovs.daemon.add_args(parser)
448     args = parser.parse_args()
449     ovs.daemon.handle_args(args)
450
451     global root_prefix
452     root_prefix = args.root_prefix
453
454     remote = args.database
455     schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
456     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
457     prune_schema(schema)
458     idl = ovs.db.idl.Idl(remote, schema)
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.tables)
473
474         new_interfaces = {}
475         for rec in idl.tables["Interface"].rows.itervalues():
476             if rec.type == "ipsec_gre":
477                 name = rec.name
478                 options = rec.options
479                 entry = {
480                     "remote_ip": options.get("remote_ip"),
481                     "local_ip": options.get("local_ip", "0.0.0.0/0"),
482                     "certificate": options.get("certificate"),
483                     "private_key": options.get("private_key"),
484                     "use_ssl_cert": options.get("use_ssl_cert"),
485                     "peer_cert": options.get("peer_cert"),
486                     "psk": options.get("psk")}
487
488                 if entry["peer_cert"] and entry["psk"]:
489                     s_log.warning("both 'peer_cert' and 'psk' defined for %s"
490                             % name)
491                     continue
492                 elif not entry["peer_cert"] and not entry["psk"]:
493                     s_log.warning("no 'peer_cert' or 'psk' defined for %s"
494                             % name)
495                     continue
496
497                 # The "use_ssl_cert" option is deprecated and will
498                 # likely go away in the near future.
499                 if entry["use_ssl_cert"] == "true":
500                     if not ssl_cert:
501                         s_log.warning("no valid SSL entry for %s" % name)
502                         continue
503
504                     entry["certificate"] = ssl_cert[0]
505                     entry["private_key"] = ssl_cert[1]
506
507                 new_interfaces[name] = entry
508
509         if interfaces != new_interfaces:
510             update_ipsec(ipsec, interfaces, new_interfaces)
511             interfaces = new_interfaces
512
513
514 if __name__ == '__main__':
515     try:
516         main()
517     except SystemExit:
518         # Let system.exit() calls complete normally
519         raise
520     except:
521         s_log.exception("traceback")
522         sys.exit(ovs.daemon.RESTART_EXIT_CODE)