]>
git.proxmox.com Git - ovs.git/blob - debian/ovs-monitor-ipsec
2 # Copyright (c) 2009, 2010, 2011, 2012 Nicira, Inc.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
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.
35 from ovs
.db
import error
40 import ovs
.unixctl
.server
42 from six
.moves
import range
45 vlog
= ovs
.vlog
.Vlog("ovs-monitor-ipsec")
46 root_prefix
= '' # Prefix for absolute file names, for testing.
47 SETKEY
= "/usr/sbin/setkey"
51 def unixctl_exit(conn
, unused_argv
, unused_aux
):
57 # Class to configure the racoon daemon, which handles IKE negotiation
59 # Default locations for files
60 conf_file
= "/etc/racoon/racoon.conf"
61 cert_dir
= "/etc/racoon/certs"
62 psk_file
= "/etc/racoon/psk.txt"
64 # Racoon configuration header we use for IKE
65 conf_header
= """# Configuration file generated by Open vSwitch
67 # Do not modify by hand!
69 path pre_shared_key "%s";
70 path certificate "%s";
74 # Racoon configuration footer we use for IKE
75 conf_footer
= """sainfo anonymous {
78 encryption_algorithm aes;
79 authentication_algorithm hmac_sha1, hmac_md5;
80 compression_algorithm deflate;
85 # Certificate entry template.
86 cert_entry
= """remote %s {
90 certificate_type x509 "%s" "%s";
92 peers_identifier asn1dn;
93 peers_certfile x509 "%s";
96 encryption_algorithm aes;
98 authentication_method rsasig;
105 # Pre-shared key template.
106 psk_entry
= """remote %s {
110 encryption_algorithm aes;
112 authentication_method pre_shared_key;
123 if not os
.path
.isdir(root_prefix
+ self
.cert_dir
):
124 os
.mkdir(self
.cert_dir
)
126 # Clean out stale peer certs from previous runs
127 for ovs_cert
in glob
.glob("%s%s/ovs-*.pem"
128 % (root_prefix
, self
.cert_dir
)):
132 vlog
.warn("couldn't remove %s" % ovs_cert
)
134 # Replace racoon's conf file with our template
138 exitcode
= subprocess
.call([root_prefix
+ "/etc/init.d/racoon",
141 # Racoon is finicky about its configuration file and will
142 # refuse to start if it sees something it doesn't like
143 # (e.g., a certificate file doesn't exist). Try restarting
144 # the process before giving up.
145 vlog
.warn("attempting to restart racoon")
146 exitcode
= subprocess
.call([root_prefix
+ "/etc/init.d/racoon",
149 vlog
.warn("couldn't reload racoon")
152 # Rewrite the Racoon configuration file
153 conf_file
= open(root_prefix
+ self
.conf_file
, 'w')
154 conf_file
.write(Racoon
.conf_header
% (self
.psk_file
, self
.cert_dir
))
156 for host
, vals
in six
.iteritems(self
.cert_hosts
):
157 conf_file
.write(Racoon
.cert_entry
% (host
, vals
["certificate"],
158 vals
["private_key"], vals
["peer_cert_file"]))
160 for host
in self
.psk_hosts
:
161 conf_file
.write(Racoon
.psk_entry
% host
)
163 conf_file
.write(Racoon
.conf_footer
)
166 # Rewrite the pre-shared keys file; it must only be readable by root.
167 orig_umask
= os
.umask(0o077)
168 psk_file
= open(root_prefix
+ Racoon
.psk_file
, 'w')
171 psk_file
.write("# Generated by Open vSwitch...do not modify by hand!")
172 psk_file
.write("\n\n")
173 for host
, vals
in six
.iteritems(self
.psk_hosts
):
174 psk_file
.write("%s %s\n" % (host
, vals
["psk"]))
179 def _add_psk(self
, host
, psk
):
180 if host
in self
.cert_hosts
:
181 raise error
.Error("host %s already defined for cert" % host
)
183 self
.psk_hosts
[host
] = psk
186 def _verify_certs(self
, vals
):
187 # Racoon will refuse to start if the certificate files don't
188 # exist, so verify that they're there.
189 if not os
.path
.isfile(root_prefix
+ vals
["certificate"]):
190 raise error
.Error("'certificate' file does not exist: %s"
191 % vals
["certificate"])
192 elif not os
.path
.isfile(root_prefix
+ vals
["private_key"]):
193 raise error
.Error("'private_key' file does not exist: %s"
194 % vals
["private_key"])
196 # Racoon won't start if a given certificate or private key isn't
197 # valid. This is a weak test, but will detect the most flagrant
199 if vals
["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
200 raise error
.Error("'peer_cert' is not in valid PEM format")
202 cert
= open(root_prefix
+ vals
["certificate"]).read()
203 if cert
.find("-----BEGIN CERTIFICATE-----") == -1:
204 raise error
.Error("'certificate' is not in valid PEM format")
206 cert
= open(root_prefix
+ vals
["private_key"]).read()
207 if cert
.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
208 raise error
.Error("'private_key' is not in valid PEM format")
210 def _add_cert(self
, host
, vals
):
211 if host
in self
.psk_hosts
:
212 raise error
.Error("host %s already defined for psk" % host
)
214 if vals
["certificate"] is None:
215 raise error
.Error("'certificate' not defined for %s" % host
)
216 elif vals
["private_key"] is None:
217 # Assume the private key is stored in the same PEM file as
218 # the certificate. We make a copy of "vals" so that we don't
219 # modify the original "vals", which would cause the script
220 # to constantly think that the configuration has changed
223 vals
["private_key"] = vals
["certificate"]
225 self
._verify
_certs
(vals
)
227 # The peer's certificate comes to us in PEM format as a string.
228 # Write that string to a file for Racoon to use.
229 f
= open(root_prefix
+ vals
["peer_cert_file"], "w")
230 f
.write(vals
["peer_cert"])
233 self
.cert_hosts
[host
] = vals
236 def _del_cert(self
, host
):
237 peer_cert_file
= self
.cert_hosts
[host
]["peer_cert_file"]
238 del self
.cert_hosts
[host
]
241 os
.remove(root_prefix
+ peer_cert_file
)
245 def add_entry(self
, host
, vals
):
246 if vals
["peer_cert"]:
247 self
._add
_cert
(host
, vals
)
249 self
._add
_psk
(host
, vals
)
251 def del_entry(self
, host
):
252 if host
in self
.cert_hosts
:
254 elif host
in self
.psk_hosts
:
255 del self
.psk_hosts
[host
]
259 # Class to configure IPsec on a system using racoon for IKE and setkey
260 # for maintaining the Security Association Database (SAD) and Security
261 # Policy Database (SPD). Only policies for GRE are supported.
266 self
.racoon
= Racoon()
269 def call_setkey(self
, cmds
):
271 p
= subprocess
.Popen([root_prefix
+ SETKEY
, "-c"],
272 stdin
=subprocess
.PIPE
,
273 stdout
=subprocess
.PIPE
)
275 vlog
.err("could not call %s%s" % (root_prefix
, SETKEY
))
278 # xxx It is safer to pass the string into the communicate()
279 # xxx method, but it didn't work for slightly longer commands.
280 # xxx An alternative may need to be found.
282 return p
.communicate()[0]
284 def get_spi(self
, local_ip
, remote_ip
, proto
="esp"):
285 # Run the setkey dump command to retrieve the SAD. Then, parse
286 # the output looking for SPI buried in the output. Note that
287 # multiple SAD entries can exist for the same "flow", since an
288 # older entry could be in a "dying" state.
290 host_line
= "%s %s" % (local_ip
, remote_ip
)
291 results
= self
.call_setkey("dump ;\n").split("\n")
292 for i
in range(len(results
)):
293 if results
[i
].strip() == host_line
:
294 # The SPI is in the line following the host pair
295 spi_line
= results
[i
+ 1]
296 if (spi_line
[1:4] == proto
):
297 spi
= spi_line
.split()[2]
298 spi_list
.append(spi
.split('(')[1].rstrip(')'))
302 self
.call_setkey("flush;\n")
304 def sad_del(self
, local_ip
, remote_ip
):
305 # To delete all SAD entries, we should be able to use setkey's
306 # "deleteall" command. Unfortunately, it's fundamentally broken
307 # on Linux and not documented as such.
310 # Delete local_ip->remote_ip SAD entries
311 spi_list
= self
.get_spi(local_ip
, remote_ip
)
313 cmds
+= "delete %s %s esp %s;\n" % (local_ip
, remote_ip
, spi
)
315 # Delete remote_ip->local_ip SAD entries
316 spi_list
= self
.get_spi(remote_ip
, local_ip
)
318 cmds
+= "delete %s %s esp %s;\n" % (remote_ip
, local_ip
, spi
)
321 self
.call_setkey(cmds
)
324 self
.call_setkey("spdflush;\n")
326 def spd_add(self
, local_ip
, remote_ip
):
327 cmds
= ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
328 (local_ip
, remote_ip
))
329 cmds
+= ("spdadd %s %s gre -P in ipsec esp/transport//require;\n" %
330 (remote_ip
, local_ip
))
331 self
.call_setkey(cmds
)
333 def spd_del(self
, local_ip
, remote_ip
):
334 cmds
= "spddelete %s %s gre -P out;\n" % (local_ip
, remote_ip
)
335 cmds
+= "spddelete %s %s gre -P in;\n" % (remote_ip
, local_ip
)
336 self
.call_setkey(cmds
)
338 def add_entry(self
, local_ip
, remote_ip
, vals
):
339 if remote_ip
in self
.entries
:
340 raise error
.Error("host %s already configured for ipsec"
343 self
.racoon
.add_entry(remote_ip
, vals
)
344 self
.spd_add(local_ip
, remote_ip
)
346 self
.entries
.append(remote_ip
)
348 def del_entry(self
, local_ip
, remote_ip
):
349 if remote_ip
in self
.entries
:
350 self
.racoon
.del_entry(remote_ip
)
351 self
.spd_del(local_ip
, remote_ip
)
352 self
.sad_del(local_ip
, remote_ip
)
354 self
.entries
.remove(remote_ip
)
357 def update_ipsec(ipsec
, interfaces
, new_interfaces
):
358 for name
, vals
in six
.iteritems(interfaces
):
359 if name
not in new_interfaces
:
360 ipsec
.del_entry(vals
["local_ip"], vals
["remote_ip"])
362 for name
, vals
in six
.iteritems(new_interfaces
):
363 orig_vals
= interfaces
.get(name
)
365 # Configuration for this host already exists. Check if it's
366 # changed. We use set difference, since we want to ignore
367 # any local additions to "orig_vals" that we've made
368 # (e.g. the "peer_cert_file" key).
369 if set(vals
.items()) - set(orig_vals
.items()):
370 ipsec
.del_entry(vals
["local_ip"], vals
["remote_ip"])
375 ipsec
.add_entry(vals
["local_ip"], vals
["remote_ip"], vals
)
376 except error
.Error
as msg
:
377 vlog
.warn("skipping ipsec config for %s: %s" % (name
, msg
))
380 def get_ssl_cert(data
):
381 for ovs_rec
in data
["Open_vSwitch"].rows
.values():
384 if ssl
.certificate
and ssl
.private_key
:
385 return (ssl
.certificate
, ssl
.private_key
)
392 parser
= argparse
.ArgumentParser()
393 parser
.add_argument("database", metavar
="DATABASE",
394 help="A socket on which ovsdb-server is listening.")
395 parser
.add_argument("--root-prefix", metavar
="DIR",
396 help="Use DIR as alternate root directory"
399 ovs
.vlog
.add_args(parser
)
400 ovs
.daemon
.add_args(parser
)
401 args
= parser
.parse_args()
402 ovs
.vlog
.handle_args(args
)
403 ovs
.daemon
.handle_args(args
)
407 root_prefix
= args
.root_prefix
409 remote
= args
.database
410 schema_helper
= ovs
.db
.idl
.SchemaHelper()
411 schema_helper
.register_columns("Interface", ["name", "type", "options"])
412 schema_helper
.register_columns("Open_vSwitch", ["ssl"])
413 schema_helper
.register_columns("SSL", ["certificate", "private_key"])
414 idl
= ovs
.db
.idl
.Idl(remote
, schema_helper
)
416 ovs
.daemon
.daemonize()
418 ovs
.unixctl
.command_register("exit", "", 0, 0, unixctl_exit
, None)
419 error
, unixctl_server
= ovs
.unixctl
.server
.UnixctlServer
.create(None)
421 ovs
.util
.ovs_fatal(error
, "could not create unixctl server", vlog
)
426 seqno
= idl
.change_seqno
# Sequence number when we last processed the db
433 if seqno
== idl
.change_seqno
:
434 poller
= ovs
.poller
.Poller()
435 unixctl_server
.wait(poller
)
439 seqno
= idl
.change_seqno
441 ssl_cert
= get_ssl_cert(idl
.tables
)
444 for rec
in six
.itervalues(idl
.tables
["Interface"].rows
):
445 if rec
.type == "ipsec_gre":
447 options
= rec
.options
448 peer_cert_name
= "ovs-%s.pem" % (options
.get("remote_ip"))
450 "remote_ip": options
.get("remote_ip"),
451 "local_ip": options
.get("local_ip", "0.0.0.0/0"),
452 "certificate": options
.get("certificate"),
453 "private_key": options
.get("private_key"),
454 "use_ssl_cert": options
.get("use_ssl_cert"),
455 "peer_cert": options
.get("peer_cert"),
456 "peer_cert_file": Racoon
.cert_dir
+ "/" + peer_cert_name
,
457 "psk": options
.get("psk")}
459 if entry
["peer_cert"] and entry
["psk"]:
460 vlog
.warn("both 'peer_cert' and 'psk' defined for %s"
463 elif not entry
["peer_cert"] and not entry
["psk"]:
464 vlog
.warn("no 'peer_cert' or 'psk' defined for %s" % name
)
467 # The "use_ssl_cert" option is deprecated and will
468 # likely go away in the near future.
469 if entry
["use_ssl_cert"] == "true":
471 vlog
.warn("no valid SSL entry for %s" % name
)
474 entry
["certificate"] = ssl_cert
[0]
475 entry
["private_key"] = ssl_cert
[1]
477 new_interfaces
[name
] = entry
479 if interfaces
!= new_interfaces
:
480 update_ipsec(ipsec
, interfaces
, new_interfaces
)
481 interfaces
= new_interfaces
483 unixctl_server
.close()
487 if __name__
== '__main__':
491 # Let system.exit() calls complete normally
494 vlog
.exception("traceback")
495 sys
.exit(ovs
.daemon
.RESTART_EXIT_CODE
)