Nodepool elements: Add a script to initialize urandom
In our Xenial images, we see unbound take a while to start because it uses openssl which uses the getrandom call which can block during early boot if the nonblocking random number generator is not yet initialized. This script uses haveged to quickly initialize the generator. This commit only includes the script, a later commit will add the rest of the necessary install steps to the element. Change-Id: I09d18a0bad6c380fd149660ebfdaf6c12730dc74
This commit is contained in:
parent
4dc6f2bc98
commit
6020537b2c
2
nodepool/elements/initialize-urandom/README.rst
Normal file
2
nodepool/elements/initialize-urandom/README.rst
Normal file
@ -0,0 +1,2 @@
|
||||
This uses haveged to quickly initialize the nonblocking kernel random
|
||||
number generator at boot.
|
@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2016 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ctypes
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
"""Add entropy to the kernel until the nonblocking pool is
|
||||
initialized.
|
||||
|
||||
The Linux kernel has 3 entropy pools: input, blocking, and
|
||||
nonblocking. Normally entropy accumulates in the input pool and as it
|
||||
is depleted by the other pools, it is transferred from the input pool
|
||||
to the others.
|
||||
|
||||
The blocking pool corresponds to /dev/random, where reads from that
|
||||
device return random numbers only as long as there is sufficient
|
||||
entropy in the blocking pool. When that entropy is depleted, further
|
||||
reads from /dev/random block until it is replenished.
|
||||
|
||||
The nonblocking pool corresponds to /dev/urandom, where reads never
|
||||
block. Even if there is no entropy in the nonblocking pool, random
|
||||
numbers are still returned.
|
||||
|
||||
The algorithms in use in Linux 3.17 require 128 bits of entropy in
|
||||
order to initialize the random number generators associated with each
|
||||
pool. Naturally, reads from /dev/random will not return until the
|
||||
associated generator is initialized. Reads from /dev/urandom will not
|
||||
block -- even if the generator is not initialized. The kernel will
|
||||
output a notice[1] if this happens.
|
||||
|
||||
In order to avoid the situation where urandom is used when
|
||||
uninitialized, the kernel diverts entropy from timers and interrupts
|
||||
to the nonblocking pool (instead of the input pool) until it is
|
||||
initialized. In this way, as the system boots, the nonblocking pool
|
||||
accumulates entropy first, reducing the time period during which
|
||||
urandom might produce numbers from an uninitialized generator, and
|
||||
then the input and blocking pools are filled.
|
||||
|
||||
Beginning with Linux 3.17, the getrandom(2) syscall was added[2] so
|
||||
that user-space programs that generally would like to use /dev/urandom
|
||||
can do so without opening a file descriptor and, more relevant here,
|
||||
can ensure that they do so only after the generator is initialized
|
||||
(which otherwise is not possible with the /dev/urandom interface).
|
||||
|
||||
Unfortunately, programs which use this interface during early boot may
|
||||
need to wait some time for the nonblocking pool to accumulate enough
|
||||
entropy to initialize, and therefore for getrandom to return.
|
||||
Particularly in the case of a VM, this may take considerable time.
|
||||
|
||||
There are many methods of addressing this shortcoming:
|
||||
|
||||
* Store data from /dev/random at shutdown and use it to seed the
|
||||
entropy pool at the next boot. Most GNU/Linux distributions do
|
||||
this. On Ubuntu Xenial, this task is performed by systemd[3].
|
||||
Unfortunately, while writes to /dev/random (which is the method
|
||||
systemd uses to seed the system at boot) do add data to the pool,
|
||||
they do not increase the internal tracking of the amount of entropy
|
||||
in the pool. Therefore, for the purposes of determining whether the
|
||||
nonblocking pool has accumulated 128 bits of entropy, they are not
|
||||
counted.
|
||||
|
||||
* Use haveged to maintain a sufficient amount of entropy. Haveged can
|
||||
produce entropy very quickly, and when run at boot, will typically
|
||||
immediately fill the entropy pool. Haveged performs an ioctl
|
||||
operation on /dev/random rather than writing data to it, and this
|
||||
ioctl allows it to specify how much entropy the data it supplies
|
||||
contains. Therefore, unlike writes to /dev/random, ioctls do
|
||||
increment the entropy counter. Unfortunately, data from ioctls are
|
||||
*always* directed to the input pool. While entropy from timers and
|
||||
interrupts are diverted to the nonblocking pool to speed its
|
||||
initialization, data arriving from the ioctl instead end up in the
|
||||
input pool for later use.
|
||||
|
||||
When more entropy than is needed is supplied to the input pool, the
|
||||
kernel will preemptively transfer some of that entropy to the
|
||||
secondary (including nonblocking) pools. Since haveged supplies so
|
||||
much data on startup, some of this entropy should be able to spill
|
||||
over into the nonblocking pool to aid it in achieving the
|
||||
initialization threshold. Unfortunately, at the stage of early boot
|
||||
we are considering, the input pool's generator also has not been
|
||||
initialized. When the kernel receives a large amount of data from
|
||||
haveged over the ioctl, it pushes the input pool's generator over
|
||||
the 128 bit threshold, and initializes the input pool's generator.
|
||||
When a pool's generator is initialized, the entropy counter for that
|
||||
pool is reset to zero. This leaves no entropy to spill over to the
|
||||
nonblocking pool. Haveged is only able to see the entropy count for
|
||||
the input pool, and therefore is unaware that further contributions
|
||||
of entropy would aid (via spill-over) in seeding the nonblocking
|
||||
pool.
|
||||
|
||||
At this point it's worth discussing why the nonblocking pool is
|
||||
still not initialized despite a full input pool. When a secondary
|
||||
pool needs more entropy, it can pull from the input pool. However,
|
||||
there is a timer that only allows the nonblocking pool to withdraw
|
||||
entropy from the input pool every 60 seconds by default (this can be
|
||||
adjusted via proc). If something during very early boot reads data
|
||||
from /dev/urandom, a transfer (from the very likely empty) input
|
||||
pool is initiated, starting the timer that will prevent another
|
||||
transfer for 60 seconds, even if the input pool is later filled
|
||||
(such as by haveged). This means that even with haveged running at
|
||||
boot the delay due to a blocking getrandom(2) call may still be as
|
||||
long as 60 seconds.
|
||||
|
||||
* Use rng-tools for the same purpose as haveged. rng-tools operates
|
||||
in a similar manner to haveged, supplying entropic data to the
|
||||
kernel via ioctl. However, it does so in smaller chunks. This
|
||||
means that once the input pool's generator surpasses the 128 bit
|
||||
threshold for initialization, entropy from the next ioctl from
|
||||
rng-tools will be available to spill over to the nonblocking pool,
|
||||
and may be sufficient to initialize it.
|
||||
|
||||
Because of this behavior, use of rng-tools may cause getrandom(2) to
|
||||
return more quickly at boot, however, this may only happen due to a
|
||||
quirk of implementation and relies on some specific values and
|
||||
conditions for the amount of entropy in the input pool at the time
|
||||
it is run.
|
||||
|
||||
This program speeds initialization of the nonblocking pool by adding
|
||||
entropy to the input pool in small chunks. To determine when the
|
||||
nonblocking pool is initialized, it performs the nonblocking
|
||||
getrandom(2) syscall requesting one byte of random data. As long as
|
||||
the nonblocking pool is uninitialized, that call will fail and set
|
||||
errno to EAGAIN. In that case, the program reads 64 bytes of data
|
||||
from haveged and sends it to the kernel using the ioctl interface,
|
||||
then repeats this in a loop. That will cause entropy to accumulate in
|
||||
the input pool until it is initialized and reaches the spill-over
|
||||
threshold. Further data will accumulate in the nonblocking pool until
|
||||
it is initialized. Once that occurs, the getrandom(2) call will
|
||||
return successfully, and the program will exit the loop.
|
||||
|
||||
There are other ways this problem could be addressed (changes to
|
||||
haveged or rng-tools to support behavior like this, or changes to the
|
||||
kernel to direct entropy received via ioctl to the nonblocking pool
|
||||
during initialization), however, this problem is likely to be
|
||||
short-lived as the nonblocking generator is being replaced[4] in
|
||||
current kernel versions and should not suffer from the same problem.
|
||||
|
||||
[1] http://lxr.free-electrons.com/source/drivers/char/random.c?v=3.17#L1385
|
||||
[2] https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=c6e9d6f38894798696f23c8084ca7edbf16ee895
|
||||
[3] https://www.freedesktop.org/software/systemd/man/systemd-random-seed.service.html
|
||||
[4] https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=e192be9d9a30555aae2ca1dc3aad37cba484cd4a
|
||||
"""
|
||||
|
||||
|
||||
class GeneratorNotInitializedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InterruptedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Pump(object):
|
||||
# How much data, in bytes, to move at once. 64 is the size of the
|
||||
# internal kernel buffer, so we match it.
|
||||
CHUNK_SIZE = 64
|
||||
|
||||
# The syscall number for getrandom(2).
|
||||
SYS_getrandom = 318
|
||||
|
||||
# The IOCTL to add entropy.
|
||||
OP_RNDADDENTROPY = 0x40085203
|
||||
|
||||
# Flags for getrandom:
|
||||
GRND_NONBLOCK = 0x0001 # Do not block
|
||||
GRND_RANDOM = 0x0002 # Use /dev/random instead of urandom
|
||||
|
||||
def __init__(self):
|
||||
# Use ctypes to invoke getrandom since it is not available in
|
||||
# python. os.urandom may call getrandom in some versions of
|
||||
# python3, however, the blocking on initialization behavior is
|
||||
# seen as a bug and so os.urandom will never block, even if
|
||||
# getrandom would. See http://bugs.python.org/issue26839
|
||||
self._getrandom = ctypes.CDLL(None, use_errno=True).syscall
|
||||
self._getrandom.restype = ctypes.c_long
|
||||
# The arguments are syscall number, void *buf,
|
||||
# size_t buflen, unsigned int flags.
|
||||
self._getrandom.argtypes = (ctypes.c_long, ctypes.c_void_p,
|
||||
ctypes.c_size_t, ctypes.c_uint)
|
||||
|
||||
def getrandom(self, length, random=False, nonblock=False):
|
||||
flags = 0
|
||||
if random:
|
||||
flags |= self.GRND_RANDOM
|
||||
if nonblock:
|
||||
flags |= self.GRND_NONBLOCK
|
||||
buf = ctypes.ARRAY(ctypes.c_char, length)()
|
||||
r = self._getrandom(self.SYS_getrandom, buf, len(buf), flags)
|
||||
if r == -1:
|
||||
err = ctypes.get_errno()
|
||||
if err == errno.EINVAL:
|
||||
raise Exception("getrandom: Invalid argument")
|
||||
elif err == errno.EFAULT:
|
||||
raise Exception("getrandom: Buffer is outside "
|
||||
"accessible address space")
|
||||
elif err == errno.EAGAIN:
|
||||
raise GeneratorNotInitializedError()
|
||||
elif err == errno.EINTR:
|
||||
raise InterruptedError()
|
||||
return buf[:r]
|
||||
|
||||
def isInitialized(self):
|
||||
# Read one byte from getrandom to determine whether the
|
||||
# nonblocking pool is initialized.
|
||||
try:
|
||||
r = self.getrandom(1, nonblock=True)
|
||||
if len(r) != 1:
|
||||
raise Exception("No data returned from getrandom")
|
||||
print("Nonblocking pool initilaized")
|
||||
return True
|
||||
except GeneratorNotInitializedError:
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
"""Move data from haveged to the kernel until the nonblocking pool is
|
||||
initialized.
|
||||
|
||||
"""
|
||||
if self.isInitialized():
|
||||
return
|
||||
|
||||
random_fd = os.open('/dev/random', os.O_RDWR)
|
||||
# Start haveged and tell it to supply unlimited data on
|
||||
# stdout, and print summary information.
|
||||
p = subprocess.Popen(['/usr/sbin/haveged', '-f', '-', '-n', '0',
|
||||
'-v', '1'],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
while not self.isInitialized():
|
||||
# Read a chunk from haveged.
|
||||
data = b''
|
||||
while len(data) < self.CHUNK_SIZE:
|
||||
data += p.stdout.read(self.CHUNK_SIZE - len(data))
|
||||
# The data structure is:
|
||||
# struct rand_pool_info {
|
||||
# int entropy_count;
|
||||
# int buf_size;
|
||||
# __u32 buf[0];
|
||||
# };
|
||||
arg = struct.pack('iis', len(data) * 8, len(data), data)
|
||||
print("Moving %s bytes" % len(data))
|
||||
fcntl.ioctl(random_fd, self.OP_RNDADDENTROPY, arg)
|
||||
# Now that the generator is initialized, stop haveged and
|
||||
# print the summary information.
|
||||
p.send_signal(2)
|
||||
p.stdout.read()
|
||||
print(p.stderr.read().decode('utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = Pump()
|
||||
p.run()
|
Loading…
x
Reference in New Issue
Block a user