]> git.proxmox.com Git - ovs.git/blob - debian/ovs-monitor-ipsec
ovs-monitor-ipsec: Add ability to traverse NATs
[ovs.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)