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