vswitch: Use "ipsec_gre" vport instead of "gre" with "other_config"
[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     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             ipsec_cert = rec.options.get("ipsec_cert")
311             ipsec_psk = rec.options.get("ipsec_psk")
312             is_ipsec = ipsec_cert or ipsec_psk
313
314             if rec.type.as_scalar() == "ipsec_gre":
315                 if ipsec_cert or ipsec_psk:
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                 else:
322                     s_log.warning(
323                         "no ipsec_cert or ipsec_psk defined for %s" % name)
324  
325         if interfaces != new_interfaces:
326             for name, vals in interfaces.items():
327                 if name not in new_interfaces.keys():
328                     ipsec.ipsec_cert_del(vals["local_ip"], vals["remote_ip"])
329             for name, vals in new_interfaces.items():
330                 orig_vals = interfaces.get(name):
331                 if orig_vals:
332                     # Configuration for this host already exists.  If
333                     # it has changed, this is an error.
334                     if vals != orig_vals:
335                         s_log.warning(
336                             "configuration changed for %s, need to delete "
337                             "interface first" % name)
338                     continue
339
340                 if vals["ipsec_cert"]:
341                     ipsec.ipsec_cert_update(vals["local_ip"],
342                             vals["remote_ip"], vals["ipsec_cert"])
343                 else:
344                     ipsec.ipsec_psk_update(vals["local_ip"], 
345                             vals["remote_ip"], vals["ipsec_psk"])
346
347             interfaces = new_interfaces
348  
349 if __name__ == '__main__':
350     try:
351         main(sys.argv)
352     except SystemExit:
353         # Let system.exit() calls complete normally
354         raise
355     except:
356         s_log.exception("traceback")
357         sys.exit(ovs.daemon.RESTART_EXIT_CODE)