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