idl: Convert python daemons to utilize SchemaHelper.
[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 os
31 import subprocess
32 import sys
33
34 import ovs.dirs
35 from ovs.db import error
36 from ovs.db import types
37 import ovs.util
38 import ovs.daemon
39 import ovs.db.idl
40 import ovs.unixctl
41 import ovs.vlog
42
43 vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
44 root_prefix = ''                # Prefix for absolute file names, for testing.
45 setkey = "/usr/sbin/setkey"
46 exiting = False
47
48
49 def unixctl_exit(conn, unused_argv, unused_aux):
50     global exiting
51     exiting = True
52     conn.reply(None)
53
54
55 # Class to configure the racoon daemon, which handles IKE negotiation
56 class Racoon:
57     # Default locations for files
58     conf_file = "/etc/racoon/racoon.conf"
59     cert_dir = "/etc/racoon/certs"
60     psk_file = "/etc/racoon/psk.txt"
61
62     # Racoon configuration header we use for IKE
63     conf_header = """# Configuration file generated by Open vSwitch
64 #
65 # Do not modify by hand!
66
67 path pre_shared_key "%s";
68 path certificate "%s";
69
70 """
71
72     # Racoon configuration footer we use for IKE
73     conf_footer = """sainfo anonymous {
74         pfs_group 2;
75         lifetime time 1 hour;
76         encryption_algorithm aes;
77         authentication_algorithm hmac_sha1, hmac_md5;
78         compression_algorithm deflate;
79 }
80
81 """
82
83     # Certificate entry template.
84     cert_entry = """remote %s {
85         exchange_mode main;
86         nat_traversal on;
87         ike_frag on;
88         certificate_type x509 "%s" "%s";
89         my_identifier asn1dn;
90         peers_identifier asn1dn;
91         peers_certfile x509 "%s";
92         verify_identifier on;
93         proposal {
94                 encryption_algorithm aes;
95                 hash_algorithm sha1;
96                 authentication_method rsasig;
97                 dh_group 2;
98         }
99 }
100
101 """
102
103     # Pre-shared key template.
104     psk_entry = """remote %s {
105         exchange_mode main;
106         nat_traversal on;
107         proposal {
108                 encryption_algorithm aes;
109                 hash_algorithm sha1;
110                 authentication_method pre_shared_key;
111                 dh_group 2;
112         }
113 }
114
115 """
116
117     def __init__(self):
118         self.psk_hosts = {}
119         self.cert_hosts = {}
120
121         if not os.path.isdir(root_prefix + self.cert_dir):
122             os.mkdir(self.cert_dir)
123
124         # Clean out stale peer certs from previous runs
125         for ovs_cert in glob.glob("%s%s/ovs-*.pem"
126                                   % (root_prefix, self.cert_dir)):
127             try:
128                 os.remove(ovs_cert)
129             except OSError:
130                 vlog.warn("couldn't remove %s" % ovs_cert)
131
132         # Replace racoon's conf file with our template
133         self.commit()
134
135     def reload(self):
136         exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
137                                     "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             vlog.warn("attempting to restart racoon")
144             exitcode = subprocess.call([root_prefix + "/etc/init.d/racoon",
145                                         "restart"])
146             if exitcode != 0:
147                 vlog.warn("couldn't reload racoon")
148
149     def commit(self):
150         # Rewrite the Racoon configuration file
151         conf_file = open(root_prefix + 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(root_prefix + 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(root_prefix + vals["certificate"]):
188             raise error.Error("'certificate' file does not exist: %s"
189                     % vals["certificate"])
190         elif not os.path.isfile(root_prefix + 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(root_prefix + 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(root_prefix + 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     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         f = open(root_prefix + vals["peer_cert_file"], "w")
228         f.write(vals["peer_cert"])
229         f.close()
230
231         self.cert_hosts[host] = vals
232         self.commit()
233
234     def _del_cert(self, host):
235         peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
236         del self.cert_hosts[host]
237         self.commit()
238         try:
239             os.remove(root_prefix + peer_cert_file)
240         except OSError:
241             pass
242
243     def add_entry(self, host, vals):
244         if vals["peer_cert"]:
245             self._add_cert(host, vals)
246         elif vals["psk"]:
247             self._add_psk(host, vals)
248
249     def del_entry(self, host):
250         if host in self.cert_hosts:
251             self._del_cert(host)
252         elif host in self.psk_hosts:
253             del self.psk_hosts[host]
254             self.commit()
255
256
257 # Class to configure IPsec on a system using racoon for IKE and setkey
258 # for maintaining the Security Association Database (SAD) and Security
259 # Policy Database (SPD).  Only policies for GRE are supported.
260 class IPsec:
261     def __init__(self):
262         self.sad_flush()
263         self.spd_flush()
264         self.racoon = Racoon()
265         self.entries = []
266
267     def call_setkey(self, cmds):
268         try:
269             p = subprocess.Popen([root_prefix + setkey, "-c"],
270                                  stdin=subprocess.PIPE,
271                                  stdout=subprocess.PIPE)
272         except:
273             vlog.err("could not call %s%s" % (root_prefix, setkey))
274             sys.exit(1)
275
276         # xxx It is safer to pass the string into the communicate()
277         # xxx method, but it didn't work for slightly longer commands.
278         # xxx An alternative may need to be found.
279         p.stdin.write(cmds)
280         return p.communicate()[0]
281
282     def get_spi(self, local_ip, remote_ip, proto="esp"):
283         # Run the setkey dump command to retrieve the SAD.  Then, parse
284         # the output looking for SPI buried in the output.  Note that
285         # multiple SAD entries can exist for the same "flow", since an
286         # older entry could be in a "dying" state.
287         spi_list = []
288         host_line = "%s %s" % (local_ip, remote_ip)
289         results = self.call_setkey("dump ;\n").split("\n")
290         for i in range(len(results)):
291             if results[i].strip() == host_line:
292                 # The SPI is in the line following the host pair
293                 spi_line = results[i + 1]
294                 if (spi_line[1:4] == proto):
295                     spi = spi_line.split()[2]
296                     spi_list.append(spi.split('(')[1].rstrip(')'))
297         return spi_list
298
299     def sad_flush(self):
300         self.call_setkey("flush;\n")
301
302     def sad_del(self, local_ip, remote_ip):
303         # To delete all SAD entries, we should be able to use setkey's
304         # "deleteall" command.  Unfortunately, it's fundamentally broken
305         # on Linux and not documented as such.
306         cmds = ""
307
308         # Delete local_ip->remote_ip SAD entries
309         spi_list = self.get_spi(local_ip, remote_ip)
310         for spi in spi_list:
311             cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
312
313         # Delete remote_ip->local_ip SAD entries
314         spi_list = self.get_spi(remote_ip, local_ip)
315         for spi in spi_list:
316             cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
317
318         if cmds:
319             self.call_setkey(cmds)
320
321     def spd_flush(self):
322         self.call_setkey("spdflush;\n")
323
324     def spd_add(self, local_ip, remote_ip):
325         cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
326                     (local_ip, remote_ip))
327         cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
328                     (remote_ip, local_ip))
329         self.call_setkey(cmds)
330
331     def spd_del(self, local_ip, remote_ip):
332         cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
333         cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
334         self.call_setkey(cmds)
335
336     def add_entry(self, local_ip, remote_ip, vals):
337         if remote_ip in self.entries:
338             raise error.Error("host %s already configured for ipsec"
339                               % remote_ip)
340
341         self.racoon.add_entry(remote_ip, vals)
342         self.spd_add(local_ip, remote_ip)
343
344         self.entries.append(remote_ip)
345
346     def del_entry(self, local_ip, remote_ip):
347         if remote_ip in self.entries:
348             self.racoon.del_entry(remote_ip)
349             self.spd_del(local_ip, remote_ip)
350             self.sad_del(local_ip, remote_ip)
351
352             self.entries.remove(remote_ip)
353
354
355 def update_ipsec(ipsec, interfaces, new_interfaces):
356     for name, vals in interfaces.iteritems():
357         if name not in new_interfaces:
358             ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
359
360     for name, vals in new_interfaces.iteritems():
361         orig_vals = interfaces.get(name)
362         if orig_vals:
363             # Configuration for this host already exists.  Check if it's
364             # changed.  We use set difference, since we want to ignore
365             # any local additions to "orig_vals" that we've made
366             # (e.g. the "peer_cert_file" key).
367             if set(vals.items()) - set(orig_vals.items()):
368                 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
369             else:
370                 continue
371
372         try:
373             ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
374         except error.Error, msg:
375             vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
376
377
378 def get_ssl_cert(data):
379     for ovs_rec in data["Open_vSwitch"].rows.itervalues():
380         if ovs_rec.ssl:
381             ssl = ovs_rec.ssl[0]
382             if ssl.certificate and ssl.private_key:
383                 return (ssl.certificate, ssl.private_key)
384
385     return None
386
387
388 def main():
389
390     parser = argparse.ArgumentParser()
391     parser.add_argument("database", metavar="DATABASE",
392                         help="A socket on which ovsdb-server is listening.")
393     parser.add_argument("--root-prefix", metavar="DIR",
394                         help="Use DIR as alternate root directory"
395                         " (for testing).")
396
397     ovs.vlog.add_args(parser)
398     ovs.daemon.add_args(parser)
399     args = parser.parse_args()
400     ovs.vlog.handle_args(args)
401     ovs.daemon.handle_args(args)
402
403     global root_prefix
404     if args.root_prefix:
405         root_prefix = args.root_prefix
406
407     remote = args.database
408     schema_helper = ovs.db.idl.SchemaHelper()
409     schema_helper.register_columns("Interface", ["name", "type", "options"])
410     schema_helper.register_columns("Open_vSwitch", ["ssl"])
411     schema_helper.register_columns("SSL", ["certificate", "private_key"])
412     idl = ovs.db.idl.Idl(remote, schema_helper)
413
414     ovs.daemon.daemonize()
415
416     ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
417     error, unixctl_server = ovs.unixctl.UnixctlServer.create(None)
418     if error:
419         ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
420
421     ipsec = IPsec()
422
423     interfaces = {}
424     while True:
425         unixctl_server.run()
426         if exiting:
427             break
428
429         if not idl.run():
430             poller = ovs.poller.Poller()
431             unixctl_server.wait(poller)
432             idl.wait(poller)
433             poller.block()
434             continue
435
436         ssl_cert = get_ssl_cert(idl.tables)
437
438         new_interfaces = {}
439         for rec in idl.tables["Interface"].rows.itervalues():
440             if rec.type == "ipsec_gre":
441                 name = rec.name
442                 options = rec.options
443                 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
444                 entry = {
445                     "remote_ip": options.get("remote_ip"),
446                     "local_ip": options.get("local_ip", "0.0.0.0/0"),
447                     "certificate": options.get("certificate"),
448                     "private_key": options.get("private_key"),
449                     "use_ssl_cert": options.get("use_ssl_cert"),
450                     "peer_cert": options.get("peer_cert"),
451                     "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
452                     "psk": options.get("psk")}
453
454                 if entry["peer_cert"] and entry["psk"]:
455                     vlog.warn("both 'peer_cert' and 'psk' defined for %s"
456                               % name)
457                     continue
458                 elif not entry["peer_cert"] and not entry["psk"]:
459                     vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
460                     continue
461
462                 # The "use_ssl_cert" option is deprecated and will
463                 # likely go away in the near future.
464                 if entry["use_ssl_cert"] == "true":
465                     if not ssl_cert:
466                         vlog.warn("no valid SSL entry for %s" % name)
467                         continue
468
469                     entry["certificate"] = ssl_cert[0]
470                     entry["private_key"] = ssl_cert[1]
471
472                 new_interfaces[name] = entry
473
474         if interfaces != new_interfaces:
475             update_ipsec(ipsec, interfaces, new_interfaces)
476             interfaces = new_interfaces
477
478     unixctl_server.close()
479     idl.close()
480
481
482 if __name__ == '__main__':
483     try:
484         main()
485     except SystemExit:
486         # Let system.exit() calls complete normally
487         raise
488     except:
489         vlog.exception("traceback")
490         sys.exit(ovs.daemon.RESTART_EXIT_CODE)