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