debian: Restart ovs-monitor-ipsec when --monitor specified
[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 #  - Doesn't support cert authentication
24
25
26 import getopt
27 import logging, logging.handlers
28 import os
29 import stat
30 import subprocess
31 import sys
32
33 from ovs.db import error
34 from ovs.db import types
35 import ovs.util
36 import ovs.daemon
37 import ovs.db.idl
38
39
40 # By default log messages as DAEMON into syslog
41 s_log = logging.getLogger("ovs-monitor-ipsec")
42 l_handler = logging.handlers.SysLogHandler(
43         "/dev/log",
44         facility=logging.handlers.SysLogHandler.LOG_DAEMON)
45 l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s')
46 l_handler.setFormatter(l_formatter)
47 s_log.addHandler(l_handler)
48
49
50 setkey = "/usr/sbin/setkey"
51
52 # Class to configure the racoon daemon, which handles IKE negotiation
53 class Racoon:
54     # Default locations for files
55     conf_file = "/etc/racoon/racoon.conf"
56     cert_file = "/etc/racoon/certs"
57     psk_file = "/etc/racoon/psk.txt"
58
59     # Default racoon configuration file we use for IKE
60     conf_template = """# Configuration file generated by Open vSwitch
61 #
62 # Do not modify by hand!
63
64 path pre_shared_key "/etc/racoon/psk.txt";
65 path certificate "/etc/racoon/certs";
66
67 remote anonymous {
68         exchange_mode main;
69         proposal {
70                 encryption_algorithm aes;
71                 hash_algorithm sha1;
72                 authentication_method pre_shared_key;
73                 dh_group 2;
74         }
75 }
76
77 sainfo anonymous {
78         pfs_group 2;
79         lifetime time 1 hour;
80         encryption_algorithm aes;
81         authentication_algorithm hmac_sha1, hmac_md5;
82         compression_algorithm deflate;
83 }
84 """
85
86     def __init__(self):
87         self.psk_hosts = {}
88         self.cert_hosts = {}
89
90         # Replace racoon's conf file with our template
91         f = open(Racoon.conf_file, "w")
92         f.write(Racoon.conf_template)
93         f.close()
94
95         # Clear out any pre-shared keys
96         self.commit_psk()
97
98         self.reload()
99
100     def reload(self):
101         exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
102         if exitcode != 0:
103             s_log.warning("couldn't reload racoon")
104
105     def commit_psk(self):
106         f = open(Racoon.psk_file, 'w')
107  
108         # The file must only be accessible by root
109         os.chmod(Racoon.psk_file, stat.S_IRUSR | stat.S_IWUSR)
110
111         f.write("# Generated by Open vSwitch...do not modify by hand!\n\n")
112         for host, psk in self.psk_hosts.iteritems():
113             f.write("%s   %s\n" % (host, psk))
114         f.close()
115
116     def add_psk(self, host, psk):
117         self.psk_hosts[host] = psk
118         self.commit_psk()
119
120     def del_psk(self, host):
121         if host in self.psk_hosts:
122             del self.psk_hosts[host]
123             self.commit_psk()
124
125
126 # Class to configure IPsec on a system using racoon for IKE and setkey
127 # for maintaining the Security Association Database (SAD) and Security
128 # Policy Database (SPD).  Only policies for GRE are supported.
129 class IPsec:
130     def __init__(self):
131         self.sad_flush()
132         self.spd_flush()
133         self.racoon = Racoon()
134
135     def call_setkey(self, cmds):
136         try:
137             p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, 
138                     stdout=subprocess.PIPE)
139         except:
140             s_log.error("could not call setkey")
141             sys.exit(1)
142
143         # xxx It is safer to pass the string into the communicate()
144         # xxx method, but it didn't work for slightly longer commands.
145         # xxx An alternative may need to be found.
146         p.stdin.write(cmds)
147         return p.communicate()[0]
148
149     def get_spi(self, local_ip, remote_ip, proto="esp"):
150         # Run the setkey dump command to retrieve the SAD.  Then, parse
151         # the output looking for SPI buried in the output.  Note that
152         # multiple SAD entries can exist for the same "flow", since an
153         # older entry could be in a "dying" state.
154         spi_list = []
155         host_line = "%s %s" % (local_ip, remote_ip)
156         results = self.call_setkey("dump ;").split("\n")
157         for i in range(len(results)):
158             if results[i].strip() == host_line:
159                 # The SPI is in the line following the host pair
160                 spi_line = results[i+1]
161                 if (spi_line[1:4] == proto):
162                     spi = spi_line.split()[2]
163                     spi_list.append(spi.split('(')[1].rstrip(')'))
164         return spi_list
165
166     def sad_flush(self):
167         self.call_setkey("flush;")
168
169     def sad_del(self, local_ip, remote_ip):
170         # To delete all SAD entries, we should be able to use setkey's
171         # "deleteall" command.  Unfortunately, it's fundamentally broken
172         # on Linux and not documented as such.
173         cmds = ""
174
175         # Delete local_ip->remote_ip SAD entries
176         spi_list = self.get_spi(local_ip, remote_ip)
177         for spi in spi_list:
178             cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
179
180         # Delete remote_ip->local_ip SAD entries
181         spi_list = self.get_spi(remote_ip, local_ip)
182         for spi in spi_list:
183             cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
184
185         if cmds:
186             self.call_setkey(cmds)
187
188     def spd_flush(self):
189         self.call_setkey("spdflush;")
190
191     def spd_add(self, local_ip, remote_ip):
192         cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;" %
193                     (local_ip, remote_ip))
194         cmds += "\n"
195         cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" %
196                     (remote_ip, local_ip))
197         self.call_setkey(cmds)
198
199     def spd_del(self, local_ip, remote_ip):
200         cmds = "spddelete %s %s gre -P out;" % (local_ip, remote_ip)
201         cmds += "\n"
202         cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
203         self.call_setkey(cmds)
204
205     def ipsec_cert_del(self, local_ip, remote_ip):
206         # Need to support cert...right now only PSK supported
207         self.racoon.del_psk(remote_ip)
208         self.spd_del(local_ip, remote_ip)
209         self.sad_del(local_ip, remote_ip)
210
211     def ipsec_cert_update(self, local_ip, remote_ip, cert):
212         # Need to support cert...right now only PSK supported
213         self.racoon.add_psk(remote_ip, "abc12345")
214         self.spd_add(local_ip, remote_ip)
215
216     def ipsec_psk_del(self, local_ip, remote_ip):
217         self.racoon.del_psk(remote_ip)
218         self.spd_del(local_ip, remote_ip)
219         self.sad_del(local_ip, remote_ip)
220
221     def ipsec_psk_update(self, local_ip, remote_ip, psk):
222         self.racoon.add_psk(remote_ip, psk)
223         self.spd_add(local_ip, remote_ip)
224
225
226 def keep_table_columns(schema, table_name, column_types):
227     table = schema.tables.get(table_name)
228     if not table:
229         raise error.Error("schema has no %s table" % table_name)
230
231     new_columns = {}
232     for column_name, column_type in column_types.iteritems():
233         column = table.columns.get(column_name)
234         if not column:
235             raise error.Error("%s table schema lacks %s column"
236                               % (table_name, column_name))
237         if column.type != column_type:
238             raise error.Error("%s column in %s table has type \"%s\", "
239                               "expected type \"%s\""
240                               % (column_name, table_name,
241                                  column.type.toEnglish(),
242                                  column_type.toEnglish()))
243         new_columns[column_name] = column
244     table.columns = new_columns
245     return table
246  
247 def monitor_uuid_schema_cb(schema):
248     string_type = types.Type(types.BaseType(types.StringType))
249     string_map_type = types.Type(types.BaseType(types.StringType),
250                                  types.BaseType(types.StringType),
251                                  0, sys.maxint)
252  
253     new_tables = {}
254     new_tables["Interface"] = keep_table_columns(
255         schema, "Interface", {"name": string_type,
256                               "type": string_type,
257                               "options": string_map_type,
258                               "other_config": string_map_type})
259     schema.tables = new_tables
260
261 def usage():
262     print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
263     print "where DATABASE is a socket on which ovsdb-server is listening."
264     ovs.daemon.usage()
265     print "Other options:"
266     print "  -h, --help               display this help message"
267     sys.exit(0)
268  
269 def main(argv):
270     try:
271         options, args = getopt.gnu_getopt(
272             argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS)
273     except getopt.GetoptError, geo:
274         sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg))
275         sys.exit(1)
276  
277     for key, value in options:
278         if key in ['-h', '--help']:
279             usage()
280         elif not ovs.daemon.parse_opt(key, value):
281             sys.stderr.write("%s: unhandled option %s\n"
282                              % (ovs.util.PROGRAM_NAME, key))
283             sys.exit(1)
284  
285     if len(args) != 1:
286         sys.stderr.write("%s: exactly one nonoption argument is required "
287                          "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
288         sys.exit(1)
289
290     ovs.daemon.die_if_already_running()
291  
292     remote = args[0]
293     idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
294
295     ovs.daemon.daemonize()
296
297     ipsec = IPsec()
298
299     interfaces = {}
300     while True:
301         if not idl.run():
302             poller = ovs.poller.Poller()
303             idl.wait(poller)
304             poller.block()
305             continue
306  
307         new_interfaces = {}
308         for rec in idl.data["Interface"].itervalues():
309             name = rec.name.as_scalar()
310             local_ip = rec.other_config.get("ipsec_local_ip")
311             if rec.type.as_scalar() == "gre" and local_ip:
312                 new_interfaces[name] = {
313                         "remote_ip": rec.options.get("remote_ip"),
314                         "local_ip": local_ip,
315                         "ipsec_cert": rec.other_config.get("ipsec_cert"),
316                         "ipsec_psk": rec.other_config.get("ipsec_psk") }
317  
318         if interfaces != new_interfaces:
319             for name, vals in interfaces.items():
320                 if name not in new_interfaces.keys():
321                     ipsec.ipsec_cert_del(vals["local_ip"], vals["remote_ip"])
322             for name, vals in new_interfaces.items():
323                 if vals == interfaces.get(name):
324                     s_log.warning(
325                         "configuration changed for %s, need to delete "
326                         "interface first" % name)
327                     continue
328
329                 if vals["ipsec_cert"]:
330                     ipsec.ipsec_cert_update(vals["local_ip"],
331                             vals["remote_ip"], vals["ipsec_cert"])
332                 elif vals["ipsec_psk"]:
333                     ipsec.ipsec_psk_update(vals["local_ip"], 
334                             vals["remote_ip"], vals["ipsec_psk"])
335                 else:
336                     s_log.warning(
337                         "no ipsec_cert or ipsec_psk defined for %s" % name)
338                     continue
339
340             interfaces = new_interfaces
341  
342 if __name__ == '__main__':
343     try:
344         main(sys.argv)
345     except SystemExit:
346         # Let system.exit() calls complete normally
347         raise
348     except:
349         s_log.exception("traceback")
350         sys.exit(ovs.daemon.RESTART_EXIT_CODE)