970708264ad0b12ece3cd23ad3609bd1206ad707
[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         peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host)
220         f = open(root_prefix + peer_cert_file, "w")
221         f.write(vals["peer_cert"])
222         f.close()
223
224         vals["peer_cert_file"] = peer_cert_file
225
226         self.cert_hosts[host] = vals
227         self.commit()
228
229     def _del_cert(self, host):
230         peer_cert_file = self.cert_hosts[host]["peer_cert_file"]
231         del self.cert_hosts[host]
232         self.commit()
233         try:
234             os.remove(root_prefix + peer_cert_file)
235         except OSError:
236             pass
237
238     def add_entry(self, host, vals):
239         if vals["peer_cert"]:
240             self._add_cert(host, vals)
241         elif vals["psk"]:
242             self._add_psk(host, vals)
243
244     def del_entry(self, host):
245         if host in self.cert_hosts:
246             self._del_cert(host)
247         elif host in self.psk_hosts:
248             del self.psk_hosts[host]
249             self.commit()
250
251
252 # Class to configure IPsec on a system using racoon for IKE and setkey
253 # for maintaining the Security Association Database (SAD) and Security
254 # Policy Database (SPD).  Only policies for GRE are supported.
255 class IPsec:
256     def __init__(self):
257         self.sad_flush()
258         self.spd_flush()
259         self.racoon = Racoon()
260         self.entries = []
261
262     def call_setkey(self, cmds):
263         try:
264             p = subprocess.Popen([root_prefix + setkey, "-c"],
265                                  stdin=subprocess.PIPE,
266                                  stdout=subprocess.PIPE)
267         except:
268             vlog.err("could not call %s%s" % (root_prefix, setkey))
269             sys.exit(1)
270
271         # xxx It is safer to pass the string into the communicate()
272         # xxx method, but it didn't work for slightly longer commands.
273         # xxx An alternative may need to be found.
274         p.stdin.write(cmds)
275         return p.communicate()[0]
276
277     def get_spi(self, local_ip, remote_ip, proto="esp"):
278         # Run the setkey dump command to retrieve the SAD.  Then, parse
279         # the output looking for SPI buried in the output.  Note that
280         # multiple SAD entries can exist for the same "flow", since an
281         # older entry could be in a "dying" state.
282         spi_list = []
283         host_line = "%s %s" % (local_ip, remote_ip)
284         results = self.call_setkey("dump ;\n").split("\n")
285         for i in range(len(results)):
286             if results[i].strip() == host_line:
287                 # The SPI is in the line following the host pair
288                 spi_line = results[i + 1]
289                 if (spi_line[1:4] == proto):
290                     spi = spi_line.split()[2]
291                     spi_list.append(spi.split('(')[1].rstrip(')'))
292         return spi_list
293
294     def sad_flush(self):
295         self.call_setkey("flush;\n")
296
297     def sad_del(self, local_ip, remote_ip):
298         # To delete all SAD entries, we should be able to use setkey's
299         # "deleteall" command.  Unfortunately, it's fundamentally broken
300         # on Linux and not documented as such.
301         cmds = ""
302
303         # Delete local_ip->remote_ip SAD entries
304         spi_list = self.get_spi(local_ip, remote_ip)
305         for spi in spi_list:
306             cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
307
308         # Delete remote_ip->local_ip SAD entries
309         spi_list = self.get_spi(remote_ip, local_ip)
310         for spi in spi_list:
311             cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
312
313         if cmds:
314             self.call_setkey(cmds)
315
316     def spd_flush(self):
317         self.call_setkey("spdflush;\n")
318
319     def spd_add(self, local_ip, remote_ip):
320         cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
321                     (local_ip, remote_ip))
322         cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
323                     (remote_ip, local_ip))
324         self.call_setkey(cmds)
325
326     def spd_del(self, local_ip, remote_ip):
327         cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip)
328         cmds += "spddelete %s %s gre -P in;\n" % (remote_ip, local_ip)
329         self.call_setkey(cmds)
330
331     def add_entry(self, local_ip, remote_ip, vals):
332         if remote_ip in self.entries:
333             raise error.Error("host %s already configured for ipsec"
334                               % remote_ip)
335
336         self.racoon.add_entry(remote_ip, vals)
337         self.spd_add(local_ip, remote_ip)
338
339         self.entries.append(remote_ip)
340
341     def del_entry(self, local_ip, remote_ip):
342         if remote_ip in self.entries:
343             self.racoon.del_entry(remote_ip)
344             self.spd_del(local_ip, remote_ip)
345             self.sad_del(local_ip, remote_ip)
346
347             self.entries.remove(remote_ip)
348
349
350 def keep_table_columns(schema, table_name, column_types):
351     table = schema.tables.get(table_name)
352     if not table:
353         raise error.Error("schema has no %s table" % table_name)
354
355     new_columns = {}
356     for column_name, column_type in column_types.iteritems():
357         column = table.columns.get(column_name)
358         if not column:
359             raise error.Error("%s table schema lacks %s column"
360                               % (table_name, column_name))
361         if column.type != column_type:
362             raise error.Error("%s column in %s table has type \"%s\", "
363                               "expected type \"%s\""
364                               % (column_name, table_name,
365                                  column.type.toEnglish(),
366                                  column_type.toEnglish()))
367         new_columns[column_name] = column
368     table.columns = new_columns
369     return table
370
371
372 def prune_schema(schema):
373     string_type = types.Type(types.BaseType(types.StringType))
374     optional_ssl_type = types.Type(types.BaseType(types.UuidType,
375         ref_table_name='SSL'), None, 0, 1)
376     string_map_type = types.Type(types.BaseType(types.StringType),
377                                  types.BaseType(types.StringType),
378                                  0, sys.maxint)
379
380     new_tables = {}
381     new_tables["Interface"] = keep_table_columns(
382         schema, "Interface", {"name": string_type,
383                               "type": string_type,
384                               "options": string_map_type})
385     new_tables["Open_vSwitch"] = keep_table_columns(
386         schema, "Open_vSwitch", {"ssl": optional_ssl_type})
387     new_tables["SSL"] = keep_table_columns(
388         schema, "SSL", {"certificate": string_type,
389                         "private_key": string_type})
390     schema.tables = new_tables
391
392
393 def update_ipsec(ipsec, interfaces, new_interfaces):
394     for name, vals in interfaces.iteritems():
395         if name not in new_interfaces:
396             ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
397
398     for name, vals in new_interfaces.iteritems():
399         orig_vals = interfaces.get(name)
400         if orig_vals:
401             # Configuration for this host already exists.  Check if it's
402             # changed.  We use set difference, since we want to ignore
403             # any local additions to "orig_vals" that we've made
404             # (e.g. the "peer_cert_file" key).
405             if set(vals.items()) - set(orig_vals.items()):
406                 ipsec.del_entry(vals["local_ip"], vals["remote_ip"])
407             else:
408                 continue
409
410         try:
411             ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals)
412         except error.Error, msg:
413             vlog.warn("skipping ipsec config for %s: %s" % (name, msg))
414
415
416 def get_ssl_cert(data):
417     for ovs_rec in data["Open_vSwitch"].rows.itervalues():
418         if ovs_rec.ssl:
419             ssl = ovs_rec.ssl[0]
420             if ssl.certificate and ssl.private_key:
421                 return (ssl.certificate, ssl.private_key)
422
423     return None
424
425
426 def main():
427
428     parser = argparse.ArgumentParser()
429     parser.add_argument("database", metavar="DATABASE",
430                         help="A socket on which ovsdb-server is listening.")
431     parser.add_argument("--root-prefix", metavar="DIR",
432                         help="Use DIR as alternate root directory"
433                         " (for testing).")
434
435     ovs.vlog.add_args(parser)
436     ovs.daemon.add_args(parser)
437     args = parser.parse_args()
438     ovs.vlog.handle_args(args)
439     ovs.daemon.handle_args(args)
440
441     global root_prefix
442     if args.root_prefix:
443         root_prefix = args.root_prefix
444
445     remote = args.database
446     schema_file = "%s/vswitch.ovsschema" % ovs.dirs.PKGDATADIR
447     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schema_file))
448     prune_schema(schema)
449     idl = ovs.db.idl.Idl(remote, schema)
450
451     ovs.daemon.daemonize()
452
453     ipsec = IPsec()
454
455     interfaces = {}
456     while True:
457         if not idl.run():
458             poller = ovs.poller.Poller()
459             idl.wait(poller)
460             poller.block()
461             continue
462
463         ssl_cert = get_ssl_cert(idl.tables)
464
465         new_interfaces = {}
466         for rec in idl.tables["Interface"].rows.itervalues():
467             if rec.type == "ipsec_gre":
468                 name = rec.name
469                 options = rec.options
470                 entry = {
471                     "remote_ip": options.get("remote_ip"),
472                     "local_ip": options.get("local_ip", "0.0.0.0/0"),
473                     "certificate": options.get("certificate"),
474                     "private_key": options.get("private_key"),
475                     "use_ssl_cert": options.get("use_ssl_cert"),
476                     "peer_cert": options.get("peer_cert"),
477                     "psk": options.get("psk")}
478
479                 if entry["peer_cert"] and entry["psk"]:
480                     vlog.warn("both 'peer_cert' and 'psk' defined for %s"
481                               % name)
482                     continue
483                 elif not entry["peer_cert"] and not entry["psk"]:
484                     vlog.warn("no 'peer_cert' or 'psk' defined for %s" % name)
485                     continue
486
487                 # The "use_ssl_cert" option is deprecated and will
488                 # likely go away in the near future.
489                 if entry["use_ssl_cert"] == "true":
490                     if not ssl_cert:
491                         vlog.warn("no valid SSL entry for %s" % name)
492                         continue
493
494                     entry["certificate"] = ssl_cert[0]
495                     entry["private_key"] = ssl_cert[1]
496
497                 new_interfaces[name] = entry
498
499         if interfaces != new_interfaces:
500             update_ipsec(ipsec, interfaces, new_interfaces)
501             interfaces = new_interfaces
502
503
504 if __name__ == '__main__':
505     try:
506         main()
507     except SystemExit:
508         # Let system.exit() calls complete normally
509         raise
510     except:
511         vlog.exception("traceback")
512         sys.exit(ovs.daemon.RESTART_EXIT_CODE)