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