e89e70922d9f6d069323b94c58b8a40b7b178f73
[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 keep_table_columns(schema, table_name, column_types):
356     table = schema.tables.get(table_name)
357     if not table:
358         raise error.Error("schema has no %s table" % table_name)
359
360     new_columns = {}
361     for column_name, column_type in column_types.iteritems():
362         column = table.columns.get(column_name)
363         if not column:
364             raise error.Error("%s table schema lacks %s column"
365                               % (table_name, column_name))
366         if column.type != column_type:
367             raise error.Error("%s column in %s table has type \"%s\", "
368                               "expected type \"%s\""
369                               % (column_name, table_name,
370                                  column.type.toEnglish(),
371                                  column_type.toEnglish()))
372         new_columns[column_name] = column
373     table.columns = new_columns
374     return table
375
376
377 def prune_schema(schema):
378     string_type = types.Type(types.BaseType(types.StringType))
379     optional_ssl_type = types.Type(types.BaseType(types.UuidType,
380         ref_table_name='SSL'), None, 0, 1)
381     string_map_type = types.Type(types.BaseType(types.StringType),
382                                  types.BaseType(types.StringType),
383                                  0, sys.maxint)
384
385     new_tables = {}
386     new_tables["Interface"] = keep_table_columns(
387         schema, "Interface", {"name": string_type,
388                               "type": string_type,
389                               "options": string_map_type})
390     new_tables["Open_vSwitch"] = keep_table_columns(
391         schema, "Open_vSwitch", {"ssl": optional_ssl_type})
392     new_tables["SSL"] = keep_table_columns(
393         schema, "SSL", {"certificate": string_type,
394                         "private_key": string_type})
395     schema.tables = new_tables
396
397
398 def update_ipsec(ipsec, interfaces, new_interfaces):
399     for name, vals in interfaces.iteritems():
400         if name not in new_interfaces:
401             ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
402
403     for name, vals in new_interfaces.iteritems():
404         orig_vals = interfaces.get(name)
405         if orig_vals:
406             # Configuration for this host already exists.  Check if it's
407             # changed.  We use set difference, since we want to ignore
408             # any local additions to "orig_vals" that we've made
409             # (e.g. the "peer_cert_file" key).
410             if set(vals.items()) - set(orig_vals.items()):
411                 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
412             else:
413                 continue
414
415         try:
416             ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
417         except error.Error, msg:
418             vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
419
420
421 def get_ssl_cert(data):
422     for ovs_rec in data["Open_vSwitch"].rows.itervalues():
423         if ovs_rec.ssl:
424             ssl = ovs_rec.ssl[0]
425             if ssl.certificate and ssl.private_key:
426                 return (ssl.certificate, ssl.private_key)
427
428     return None
429
430
431 def main():
432
433     parser = argparse.ArgumentParser()
434     parser.add_argument("database", metavar="DATABASE",
435                         help="A socket on which ovsdb-server is listening.")
436     parser.add_argument("--root-prefix", metavar="DIR",
437                         help="Use DIR as alternate root directory"
438                         " (for testing).")
439
440     ovs.vlog.add_args(parser)
441     ovs.daemon.add_args(parser)
442     args = parser.parse_args()
443     ovs.vlog.handle_args(args)
444     ovs.daemon.handle_args(args)
445
446     global root_prefix
447     if args.root_prefix:
448         root_prefix = args.root_prefix
449
450     remote = args.database
451     schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
452     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
453     prune_schema(schema)
454     idl = ovs.db.idl.Idl(remote, schema)
455
456     ovs.daemon.daemonize()
457
458     ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
459     error, unixctl_server = ovs.unixctl.UnixctlServer.create(None)
460     if error:
461         ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
462
463     ipsec = IPsec()
464
465     interfaces = {}
466     while True:
467         unixctl_server.run()
468         if exiting:
469             break
470
471         if not idl.run():
472             poller = ovs.poller.Poller()
473             unixctl_server.wait(poller)
474             idl.wait(poller)
475             poller.block()
476             continue
477
478         ssl_cert = get_ssl_cert(idl.tables)
479
480         new_interfaces = {}
481         for rec in idl.tables["Interface"].rows.itervalues():
482             if rec.type == "ipsec_gre":
483                 name = rec.name
484                 options = rec.options
485                 peer_cert_name = "ovs-%s.pem" % (options.get("remote_ip"))
486                 entry = {
487                     "remote_ip": options.get("remote_ip"),
488                     "local_ip": options.get("local_ip", "0.0.0.0/0"),
489                     "certificate": options.get("certificate"),
490                     "private_key": options.get("private_key"),
491                     "use_ssl_cert": options.get("use_ssl_cert"),
492                     "peer_cert": options.get("peer_cert"),
493                     "peer_cert_file": Racoon.cert_dir + "/" + peer_cert_name,
494                     "psk": options.get("psk")}
495
496                 if entry["peer_cert"] and entry["psk"]:
497                     vlog.warn("both 'peer_cert' and 'psk' defined for %s"
498                               % name)
499                     continue
500                 elif not entry["peer_cert"] and not entry["psk"]:
501                     vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
502                     continue
503
504                 # The "use_ssl_cert" option is deprecated and will
505                 # likely go away in the near future.
506                 if entry["use_ssl_cert"] == "true":
507                     if not ssl_cert:
508                         vlog.warn("no valid SSL entry for %s" % name)
509                         continue
510
511                     entry["certificate"] = ssl_cert[0]
512                     entry["private_key"] = ssl_cert[1]
513
514                 new_interfaces[name] = entry
515
516         if interfaces != new_interfaces:
517             update_ipsec(ipsec, interfaces, new_interfaces)
518             interfaces = new_interfaces
519
520     unixctl_server.close()
521     idl.close()
522
523
524 if __name__ == '__main__':
525     try:
526         main()
527     except SystemExit:
528         # Let system.exit() calls complete normally
529         raise
530     except:
531         vlog.exception("traceback")
532         sys.exit(ovs.daemon.RESTART_EXIT_CODE)