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