]>
git.proxmox.com Git - ovs.git/blob - debian/ovs-monitor-ipsec
2 # Copyright (c) 2009, 2010, 2011 Nicira Networks
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.
30 import logging
, logging
.handlers
35 from ovs
.db
import error
36 from ovs
.db
import types
42 # By default log messages as DAEMON into syslog
43 s_log
= logging
.getLogger("ovs-monitor-ipsec")
44 l_handler
= logging
.handlers
.SysLogHandler(
46 facility
=logging
.handlers
.SysLogHandler
.LOG_DAEMON
)
47 l_formatter
= logging
.Formatter('%(filename)s: %(levelname)s: %(message)s')
48 l_handler
.setFormatter(l_formatter
)
49 s_log
.addHandler(l_handler
)
52 setkey
= "/usr/sbin/setkey"
54 # Class to configure the racoon daemon, which handles IKE negotiation
56 # Default locations for files
57 conf_file
= "/etc/racoon/racoon.conf"
58 cert_dir
= "/etc/racoon/certs"
59 psk_file
= "/etc/racoon/psk.txt"
61 # Racoon configuration header we use for IKE
62 conf_header
= """# Configuration file generated by Open vSwitch
64 # Do not modify by hand!
66 path pre_shared_key "%s";
67 path certificate "%s";
71 # Racoon configuration footer we use for IKE
72 conf_footer
= """sainfo anonymous {
75 encryption_algorithm aes;
76 authentication_algorithm hmac_sha1, hmac_md5;
77 compression_algorithm deflate;
82 # Certificate entry template.
83 cert_entry
= """remote %s {
86 certificate_type x509 "%s" "%s";
88 peers_identifier asn1dn;
89 peers_certfile x509 "%s";
92 encryption_algorithm aes;
94 authentication_method rsasig;
101 # Pre-shared key template.
102 psk_entry
= """remote %s {
106 encryption_algorithm aes;
108 authentication_method pre_shared_key;
119 if not os
.path
.isdir(self
.cert_dir
):
120 os
.mkdir(self
.cert_dir
)
122 # Clean out stale peer certs from previous runs
123 for ovs_cert
in glob
.glob("%s/ovs-*.pem" % self
.cert_dir
):
127 s_log
.warning("couldn't remove %s" % ovs_cert
)
129 # Replace racoon's conf file with our template
133 exitcode
= subprocess
.call(["/etc/init.d/racoon", "reload"])
135 # Racoon is finicky about it's configuration file and will
136 # refuse to start if it sees something it doesn't like
137 # (e.g., a certificate file doesn't exist). Try restarting
138 # the process before giving up.
139 s_log
.warning("attempting to restart racoon")
140 exitcode
= subprocess
.call(["/etc/init.d/racoon", "restart"])
142 s_log
.warning("couldn't reload racoon")
145 # Rewrite the Racoon configuration file
146 conf_file
= open(self
.conf_file
, 'w')
147 conf_file
.write(Racoon
.conf_header
% (self
.psk_file
, self
.cert_dir
))
149 for host
, vals
in self
.cert_hosts
.iteritems():
150 conf_file
.write(Racoon
.cert_entry
% (host
, vals
["certificate"],
151 vals
["private_key"], vals
["peer_cert_file"]))
153 for host
in self
.psk_hosts
:
154 conf_file
.write(Racoon
.psk_entry
% host
)
156 conf_file
.write(Racoon
.conf_footer
)
159 # Rewrite the pre-shared keys file; it must only be readable by root.
160 orig_umask
= os
.umask(0077)
161 psk_file
= open(Racoon
.psk_file
, 'w')
164 psk_file
.write("# Generated by Open vSwitch...do not modify by hand!")
165 psk_file
.write("\n\n")
166 for host
, vals
in self
.psk_hosts
.iteritems():
167 psk_file
.write("%s %s\n" % (host
, vals
["psk"]))
172 def _add_psk(self
, host
, psk
):
173 if host
in self
.cert_hosts
:
174 raise error
.Error("host %s already defined for cert" % host
)
176 self
.psk_hosts
[host
] = psk
179 def _verify_certs(self
, vals
):
180 # Racoon will refuse to start if the certificate files don't
181 # exist, so verify that they're there.
182 if not os
.path
.isfile(vals
["certificate"]):
183 raise error
.Error("'certificate' file does not exist: %s"
184 % vals
["certificate"])
185 elif not os
.path
.isfile(vals
["private_key"]):
186 raise error
.Error("'private_key' file does not exist: %s"
187 % vals
["private_key"])
189 # Racoon won't start if a given certificate or private key isn't
190 # valid. This is a weak test, but will detect the most flagrant
192 if vals
["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1:
193 raise error
.Error("'peer_cert' is not in valid PEM format")
195 cert
= open(vals
["certificate"]).read()
196 if cert
.find("-----BEGIN CERTIFICATE-----") == -1:
197 raise error
.Error("'certificate' is not in valid PEM format")
199 cert
= open(vals
["private_key"]).read()
200 if cert
.find("-----BEGIN RSA PRIVATE KEY-----") == -1:
201 raise error
.Error("'private_key' is not in valid PEM format")
204 def _add_cert(self
, host
, vals
):
205 if host
in self
.psk_hosts
:
206 raise error
.Error("host %s already defined for psk" % host
)
208 if vals
["certificate"] == None:
209 raise error
.Error("'certificate' not defined for %s" % host
)
210 elif vals
["private_key"] == None:
211 # Assume the private key is stored in the same PEM file as
212 # the certificate. We make a copy of "vals" so that we don't
213 # modify the original "vals", which would cause the script
214 # to constantly think that the configuration has changed
217 vals
["private_key"] = vals
["certificate"]
219 self
._verify
_certs
(vals
)
221 # The peer's certificate comes to us in PEM format as a string.
222 # Write that string to a file for Racoon to use.
223 peer_cert_file
= "%s/ovs-%s.pem" % (self
.cert_dir
, host
)
224 f
= open(peer_cert_file
, "w")
225 f
.write(vals
["peer_cert"])
228 vals
["peer_cert_file"] = peer_cert_file
230 self
.cert_hosts
[host
] = vals
233 def _del_cert(self
, host
):
234 peer_cert_file
= self
.cert_hosts
[host
]["peer_cert_file"]
235 del self
.cert_hosts
[host
]
238 os
.remove(peer_cert_file
)
242 def add_entry(self
, host
, vals
):
243 if vals
["peer_cert"]:
244 self
._add
_cert
(host
, vals
)
246 self
._add
_psk
(host
, vals
)
248 def del_entry(self
, host
):
249 if host
in self
.cert_hosts
:
251 elif host
in self
.psk_hosts
:
252 del self
.psk_hosts
[host
]
256 # Class to configure IPsec on a system using racoon for IKE and setkey
257 # for maintaining the Security Association Database (SAD) and Security
258 # Policy Database (SPD). Only policies for GRE are supported.
263 self
.racoon
= Racoon()
266 def call_setkey(self
, cmds
):
268 p
= subprocess
.Popen([setkey
, "-c"], stdin
=subprocess
.PIPE
,
269 stdout
=subprocess
.PIPE
)
271 s_log
.error("could not call setkey")
274 # xxx It is safer to pass the string into the communicate()
275 # xxx method, but it didn't work for slightly longer commands.
276 # xxx An alternative may need to be found.
278 return p
.communicate()[0]
280 def get_spi(self
, local_ip
, remote_ip
, proto
="esp"):
281 # Run the setkey dump command to retrieve the SAD. Then, parse
282 # the output looking for SPI buried in the output. Note that
283 # multiple SAD entries can exist for the same "flow", since an
284 # older entry could be in a "dying" state.
286 host_line
= "%s %s" % (local_ip
, remote_ip
)
287 results
= self
.call_setkey("dump ;").split("\n")
288 for i
in range(len(results
)):
289 if results
[i
].strip() == host_line
:
290 # The SPI is in the line following the host pair
291 spi_line
= results
[i
+1]
292 if (spi_line
[1:4] == proto
):
293 spi
= spi_line
.split()[2]
294 spi_list
.append(spi
.split('(')[1].rstrip(')'))
298 self
.call_setkey("flush;")
300 def sad_del(self
, local_ip
, remote_ip
):
301 # To delete all SAD entries, we should be able to use setkey's
302 # "deleteall" command. Unfortunately, it's fundamentally broken
303 # on Linux and not documented as such.
306 # Delete local_ip->remote_ip SAD entries
307 spi_list
= self
.get_spi(local_ip
, remote_ip
)
309 cmds
+= "delete %s %s esp %s;\n" % (local_ip
, remote_ip
, spi
)
311 # Delete remote_ip->local_ip SAD entries
312 spi_list
= self
.get_spi(remote_ip
, local_ip
)
314 cmds
+= "delete %s %s esp %s;\n" % (remote_ip
, local_ip
, spi
)
317 self
.call_setkey(cmds
)
320 self
.call_setkey("spdflush;")
322 def spd_add(self
, local_ip
, remote_ip
):
323 cmds
= ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" %
324 (local_ip
, remote_ip
))
325 cmds
+= ("spdadd %s %s gre -P in ipsec esp/transport//require;" %
326 (remote_ip
, local_ip
))
327 self
.call_setkey(cmds
)
329 def spd_del(self
, local_ip
, remote_ip
):
330 cmds
= "spddelete %s %s gre -P out;\n" % (local_ip
, remote_ip
)
331 cmds
+= "spddelete %s %s gre -P in;" % (remote_ip
, local_ip
)
332 self
.call_setkey(cmds
)
334 def add_entry(self
, local_ip
, remote_ip
, vals
):
335 if remote_ip
in self
.entries
:
336 raise error
.Error("host %s already configured for ipsec"
339 self
.racoon
.add_entry(remote_ip
, vals
)
340 self
.spd_add(local_ip
, remote_ip
)
342 self
.entries
.append(remote_ip
)
345 def del_entry(self
, local_ip
, remote_ip
):
346 if remote_ip
in self
.entries
:
347 self
.racoon
.del_entry(remote_ip
)
348 self
.spd_del(local_ip
, remote_ip
)
349 self
.sad_del(local_ip
, remote_ip
)
351 self
.entries
.remove(remote_ip
)
354 def keep_table_columns(schema
, table_name
, column_types
):
355 table
= schema
.tables
.get(table_name
)
357 raise error
.Error("schema has no %s table" % table_name
)
360 for column_name
, column_type
in column_types
.iteritems():
361 column
= table
.columns
.get(column_name
)
363 raise error
.Error("%s table schema lacks %s column"
364 % (table_name
, column_name
))
365 if column
.type != column_type
:
366 raise error
.Error("%s column in %s table has type \"%s\", "
367 "expected type \"%s\""
368 % (column_name
, table_name
,
369 column
.type.toEnglish(),
370 column_type
.toEnglish()))
371 new_columns
[column_name
] = column
372 table
.columns
= new_columns
375 def monitor_uuid_schema_cb(schema
):
376 string_type
= types
.Type(types
.BaseType(types
.StringType
))
377 optional_ssl_type
= types
.Type(types
.BaseType(types
.UuidType
,
378 ref_table
='SSL'), None, 0, 1)
379 string_map_type
= types
.Type(types
.BaseType(types
.StringType
),
380 types
.BaseType(types
.StringType
),
384 new_tables
["Interface"] = keep_table_columns(
385 schema
, "Interface", {"name": string_type
,
387 "options": string_map_type
})
388 new_tables
["Open_vSwitch"] = keep_table_columns(
389 schema
, "Open_vSwitch", {"ssl": optional_ssl_type
})
390 new_tables
["SSL"] = keep_table_columns(
391 schema
, "SSL", {"certificate": string_type
,
392 "private_key": string_type
})
393 schema
.tables
= new_tables
396 print "usage: %s [OPTIONS] DATABASE" % sys
.argv
[0]
397 print "where DATABASE is a socket on which ovsdb-server is listening."
399 print "Other options:"
400 print " -h, --help display this help message"
403 def update_ipsec(ipsec
, interfaces
, new_interfaces
):
404 for name
, vals
in interfaces
.iteritems():
405 if name
not in new_interfaces
:
406 ipsec
.del_entry(vals
["local_ip"], vals
["remote_ip"])
408 for name
, vals
in new_interfaces
.iteritems():
409 orig_vals
= interfaces
.get(name
)
411 # Configuration for this host already exists. Check if it's
413 if vals
== orig_vals
:
416 ipsec
.del_entry(vals
["local_ip"], vals
["remote_ip"])
419 ipsec
.add_entry(vals
["local_ip"], vals
["remote_ip"], vals
)
420 except error
.Error
, msg
:
421 s_log
.warning("skipping ipsec config for %s: %s" % (name
, msg
))
423 def get_ssl_cert(data
):
424 for ovs_rec
in data
["Open_vSwitch"].itervalues():
425 if ovs_rec
.ssl
.as_list():
426 ssl_rec
= data
["SSL"][ovs_rec
.ssl
.as_scalar()]
427 return (ssl_rec
.certificate
.as_scalar(),
428 ssl_rec
.private_key
.as_scalar())
434 options
, args
= getopt
.gnu_getopt(
435 argv
[1:], 'h', ['help'] + ovs
.daemon
.LONG_OPTIONS
)
436 except getopt
.GetoptError
, geo
:
437 sys
.stderr
.write("%s: %s\n" % (ovs
.util
.PROGRAM_NAME
, geo
.msg
))
440 for key
, value
in options
:
441 if key
in ['-h', '--help']:
443 elif not ovs
.daemon
.parse_opt(key
, value
):
444 sys
.stderr
.write("%s: unhandled option %s\n"
445 % (ovs
.util
.PROGRAM_NAME
, key
))
449 sys
.stderr
.write("%s: exactly one nonoption argument is required "
450 "(use --help for help)\n" % ovs
.util
.PROGRAM_NAME
)
454 idl
= ovs
.db
.idl
.Idl(remote
, "Open_vSwitch", monitor_uuid_schema_cb
)
456 ovs
.daemon
.daemonize()
463 poller
= ovs
.poller
.Poller()
468 ssl_cert
= get_ssl_cert(idl
.data
)
471 for rec
in idl
.data
["Interface"].itervalues():
472 if rec
.type.as_scalar() == "ipsec_gre":
473 name
= rec
.name
.as_scalar()
475 "remote_ip": rec
.options
.get("remote_ip"),
476 "local_ip": rec
.options
.get("local_ip", "0.0.0.0/0"),
477 "certificate": rec
.options
.get("certificate"),
478 "private_key": rec
.options
.get("private_key"),
479 "use_ssl_cert": rec
.options
.get("use_ssl_cert"),
480 "peer_cert": rec
.options
.get("peer_cert"),
481 "psk": rec
.options
.get("psk") }
483 if entry
["peer_cert"] and entry
["psk"]:
484 s_log
.warning("both 'peer_cert' and 'psk' defined for %s"
487 elif not entry
["peer_cert"] and not entry
["psk"]:
488 s_log
.warning("no 'peer_cert' or 'psk' defined for %s"
492 # The "use_ssl_cert" option is deprecated and will
493 # likely go away in the near future.
494 if entry
["use_ssl_cert"] == "true":
496 s_log
.warning("no valid SSL entry for %s" % name
)
499 entry
["certificate"] = ssl_cert
[0]
500 entry
["private_key"] = ssl_cert
[1]
502 new_interfaces
[name
] = entry
504 if interfaces
!= new_interfaces
:
505 update_ipsec(ipsec
, interfaces
, new_interfaces
)
506 interfaces
= new_interfaces
508 if __name__
== '__main__':
512 # Let system.exit() calls complete normally
515 s_log
.exception("traceback")
516 sys
.exit(ovs
.daemon
.RESTART_EXIT_CODE
)