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