Files
libguestfs/p2v/nbd.c
Pino Toscano 5036ec67d5 p2v: fix possible close(-1) issue
Make sure the error handler (i.e. the code after the 'cleanup' label)
does not attempt to call close(-1), in case 'sockfd' is not initialized
yet.
2017-03-06 15:04:25 +01:00

841 lines
22 KiB
C

/* virt-p2v
* Copyright (C) 2009-2017 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/**
* This file handles the virt-p2v I<--nbd> command line option
* and running either L<qemu-nbd(8)> or L<nbdkit(1)>.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <netdb.h>
#include <errno.h>
#include <error.h>
#include <libintl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include "getprogname.h"
#include "p2v.h"
/* How long to wait for the NBD server to start (seconds). */
#define WAIT_NBD_TIMEOUT 10
/* The local port that the NBD server listens on (incremented for
* each server which is started).
*/
static int nbd_local_port;
/* List of servers specified by the --nbd option. */
enum nbd_server {
/* 0 is reserved for "end of list" */
QEMU_NBD = 1,
QEMU_NBD_NO_SA = 2,
NBDKIT = 3,
NBDKIT_NO_SA = 4,
};
static enum nbd_server *cmdline_servers = NULL;
static const char *
nbd_server_string (enum nbd_server s)
{
const char *ret = NULL;
switch (s) {
case QEMU_NBD: ret = "qemu-nbd"; break;
case QEMU_NBD_NO_SA: ret = "qemu-nbd-no-sa"; break;
case NBDKIT: ret = "nbdkit"; break;
case NBDKIT_NO_SA: ret = "nbdkit-no-sa"; break;
}
if (ret == NULL)
abort ();
return ret;
}
/* If no --nbd option is passed, we use this standard list instead.
* Must match the documentation in virt-p2v(1).
*/
static const enum nbd_server standard_servers[] =
{ QEMU_NBD, QEMU_NBD_NO_SA, NBDKIT, NBDKIT_NO_SA, 0 };
/* After testing the list of servers passed by the user, this is
* server we decide to use.
*/
static enum nbd_server use_server;
static pid_t start_qemu_nbd (const char *device, const char *ipaddr, int port, int *fds, size_t nr_fds);
static pid_t start_nbdkit (const char *device, const char *ipaddr, int port, int *fds, size_t nr_fds);
static int get_local_port (void);
static int open_listening_socket (const char *ipaddr, int **fds, size_t *nr_fds);
static int bind_tcpip_socket (const char *ipaddr, const char *port, int **fds, size_t *nr_fds);
static int connect_with_source_port (const char *hostname, int dest_port, int source_port);
static int bind_source_port (int sockfd, int family, int source_port);
static char *nbd_error;
static void set_nbd_error (const char *fs, ...)
__attribute__((format(printf,1,2)));
static void
set_nbd_error (const char *fs, ...)
{
va_list args;
char *msg;
int len;
va_start (args, fs);
len = vasprintf (&msg, fs, args);
va_end (args);
if (len < 0)
error (EXIT_FAILURE, errno,
"vasprintf (original error format string: %s)", fs);
free (nbd_error);
nbd_error = msg;
}
const char *
get_nbd_error (void)
{
return nbd_error;
}
/**
* The main program calls this to set the I<--nbd> option.
*/
void
set_nbd_option (const char *opt)
{
size_t i, len;
CLEANUP_FREE_STRING_LIST char **strs = NULL;
if (cmdline_servers != NULL)
error (EXIT_FAILURE, 0, _("--nbd option appears multiple times"));
strs = guestfs_int_split_string (',', opt);
if (strs == NULL)
error (EXIT_FAILURE, errno, _("malloc"));
len = guestfs_int_count_strings (strs);
if (len == 0)
error (EXIT_FAILURE, 0, _("--nbd option cannot be empty"));
cmdline_servers = malloc (sizeof (enum nbd_server) * (len + 1));
if (cmdline_servers == NULL)
error (EXIT_FAILURE, errno, _("malloc"));
for (i = 0; strs[i] != NULL; ++i) {
if (STREQ (strs[i], "qemu-nbd") || STREQ (strs[i], "qemu"))
cmdline_servers[i] = QEMU_NBD;
else if (STREQ (strs[i], "qemu-nbd-no-sa") || STREQ (strs[i], "qemu-no-sa"))
cmdline_servers[i] = QEMU_NBD_NO_SA;
else if (STREQ (strs[i], "nbdkit"))
cmdline_servers[i] = NBDKIT;
else if (STREQ (strs[i], "nbdkit-no-sa"))
cmdline_servers[i] = NBDKIT_NO_SA;
else
error (EXIT_FAILURE, 0, _("--nbd: unknown server: %s"), strs[i]);
}
assert (i == len);
cmdline_servers[i] = 0; /* marks the end of the list */
}
/**
* Test the I<--nbd> option (or built-in default list) to see which
* servers are actually installed and appear to be working.
*
* Set the C<use_server> global accordingly.
*/
void
test_nbd_servers (void)
{
size_t i;
int r;
const enum nbd_server *servers;
/* Initialize nbd_local_port. */
if (is_iso_environment)
/* The p2v ISO should allow us to open up just about any port, so
* we can fix a port number in that case. Using a predictable
* port number in this case should avoid rare errors if the port
* colides with another (ie. it'll either always fail or never
* fail).
*/
nbd_local_port = 50123;
else
/* When testing on the local machine, choose a random port. */
nbd_local_port = 50000 + (random () % 10000);
if (cmdline_servers != NULL)
servers = cmdline_servers;
else
servers = standard_servers;
use_server = 0;
for (i = 0; servers[i] != 0; ++i) {
#if DEBUG_STDERR
fprintf (stderr, "checking for %s ...\n", nbd_server_string (servers[i]));
#endif
switch (servers[i]) {
case QEMU_NBD: /* with socket activation */
r = system ("qemu-nbd --version"
#ifndef DEBUG_STDERR
" >/dev/null 2>&1"
#endif
" && grep -sq LISTEN_PID `which qemu-nbd`"
);
if (r == 0) {
use_server = servers[i];
goto finish;
}
break;
case QEMU_NBD_NO_SA:
r = system ("qemu-nbd --version"
#ifndef DEBUG_STDERR
" >/dev/null 2>&1"
#endif
);
if (r == 0) {
use_server = servers[i];
goto finish;
}
break;
case NBDKIT: /* with socket activation */
r = system ("nbdkit file --version"
#ifndef DEBUG_STDERR
" >/dev/null 2>&1"
#endif
" && grep -sq LISTEN_PID `which nbdkit`"
);
if (r == 0) {
use_server = servers[i];
goto finish;
}
break;
case NBDKIT_NO_SA:
r = system ("nbdkit file --version"
#ifndef DEBUG_STDERR
" >/dev/null 2>&1"
#endif
);
if (r == 0) {
use_server = servers[i];
goto finish;
}
break;
default:
abort ();
}
}
finish:
if (use_server == 0) {
fprintf (stderr,
_("%s: no working NBD server was found, cannot continue.\n"
"Please check the --nbd option in the virt-p2v(1) man page.\n"),
getprogname ());
exit (EXIT_FAILURE);
}
/* Release memory used by the --nbd option. */
free (cmdline_servers);
cmdline_servers = NULL;
#if DEBUG_STDERR
fprintf (stderr, "picked %s\n", nbd_server_string (use_server));
#endif
}
/**
* Start the NBD server.
*
* We previously tested all NBD servers (see C<test_nbd_servers>) and
* hopefully found one which will work.
*
* Returns the process ID (E<gt> 0) or C<0> if there is an error.
*/
pid_t
start_nbd_server (const char **ipaddr, int *port, const char *device)
{
int *fds = NULL;
size_t i, nr_fds;
pid_t pid;
switch (use_server) {
case QEMU_NBD: /* qemu-nbd with socket activation */
/* Ideally we would bind this socket to "localhost", but that
* requires two listening FDs, and qemu-nbd currently cannot
* support socket activation with two FDs. So we only bind to the
* IPv4 address.
*/
*ipaddr = "127.0.0.1";
*port = open_listening_socket (*ipaddr, &fds, &nr_fds);
if (*port == -1) return -1;
pid = start_qemu_nbd (device, *ipaddr, *port, fds, nr_fds);
for (i = 0; i < nr_fds; ++i)
close (fds[i]);
free (fds);
return pid;
case QEMU_NBD_NO_SA: /* qemu-nbd without socket activation */
*ipaddr = "localhost";
*port = get_local_port ();
if (*port == -1) return -1;
return start_qemu_nbd (device, *ipaddr, *port, NULL, 0);
case NBDKIT: /* nbdkit with socket activation */
*ipaddr = "localhost";
*port = open_listening_socket (*ipaddr, &fds, &nr_fds);
if (*port == -1) return -1;
pid = start_nbdkit (device, *ipaddr, *port, fds, nr_fds);
for (i = 0; i < nr_fds; ++i)
close (fds[i]);
free (fds);
return pid;
case NBDKIT_NO_SA: /* nbdkit without socket activation */
*ipaddr = "localhost";
*port = get_local_port ();
if (*port == -1) return -1;
return start_nbdkit (device, *ipaddr, *port, NULL, 0);
}
abort ();
}
#define FIRST_SOCKET_ACTIVATION_FD 3
/**
* Set up file descriptors and environment variables for
* socket activation.
*
* Note this function runs in the child between fork and exec.
*/
static inline void
socket_activation (int *fds, size_t nr_fds)
{
size_t i;
char nr_fds_str[16];
char pid_str[16];
if (fds == NULL) return;
for (i = 0; i < nr_fds; ++i) {
int fd = FIRST_SOCKET_ACTIVATION_FD + i;
if (fds[i] != fd) {
dup2 (fds[i], fd);
close (fds[i]);
}
}
snprintf (nr_fds_str, sizeof nr_fds_str, "%zu", nr_fds);
setenv ("LISTEN_FDS", nr_fds_str, 1);
snprintf (pid_str, sizeof pid_str, "%d", (int) getpid ());
setenv ("LISTEN_PID", pid_str, 1);
}
/**
* Start a local L<qemu-nbd(1)> process.
*
* If we are using socket activation, C<fds> and C<nr_fds> will
* contain the locally pre-opened file descriptors for this.
* Otherwise if C<fds == NULL> we pass the port number.
*
* Returns the process ID (E<gt> 0) or C<0> if there is an error.
*/
static pid_t
start_qemu_nbd (const char *device,
const char *ipaddr, int port, int *fds, size_t nr_fds)
{
pid_t pid;
char port_str[64];
#if DEBUG_STDERR
fprintf (stderr, "starting qemu-nbd for %s on %s:%d%s\n",
device, ipaddr, port,
fds == NULL ? "" : " using socket activation");
#endif
snprintf (port_str, sizeof port_str, "%d", port);
pid = fork ();
if (pid == -1) {
set_nbd_error ("fork: %m");
return 0;
}
if (pid == 0) { /* Child. */
close (0);
if (open ("/dev/null", O_RDONLY) == -1) {
perror ("open: /dev/null");
_exit (EXIT_FAILURE);
}
if (fds == NULL) { /* without socket activation */
execlp ("qemu-nbd",
"qemu-nbd",
"-r", /* readonly (vital!) */
"-p", port_str, /* listening port */
"-t", /* persistent */
"-f", "raw", /* force raw format */
"-b", ipaddr, /* listen only on loopback interface */
"--cache=unsafe", /* use unsafe caching for speed */
device, /* a device like /dev/sda */
NULL);
perror ("qemu-nbd");
_exit (EXIT_FAILURE);
}
else { /* socket activation */
socket_activation (fds, nr_fds);
execlp ("qemu-nbd",
"qemu-nbd",
"-r", /* readonly (vital!) */
"-t", /* persistent */
"-f", "raw", /* force raw format */
"--cache=unsafe", /* use unsafe caching for speed */
device, /* a device like /dev/sda */
NULL);
perror ("qemu-nbd");
_exit (EXIT_FAILURE);
}
}
/* Parent. */
return pid;
}
/**
* Start a local L<nbdkit(1)> process using the
* L<nbdkit-file-plugin(1)>.
*
* If we are using socket activation, C<fds> and C<nr_fds> will
* contain the locally pre-opened file descriptors for this.
* Otherwise if C<fds == NULL> we pass the port number.
*
* Returns the process ID (E<gt> 0) or C<0> if there is an error.
*/
static pid_t
start_nbdkit (const char *device,
const char *ipaddr, int port, int *fds, size_t nr_fds)
{
pid_t pid;
char port_str[64];
CLEANUP_FREE char *file_str = NULL;
#if DEBUG_STDERR
fprintf (stderr, "starting nbdkit for %s on %s:%d%s\n",
device, ipaddr, port,
fds == NULL ? "" : " using socket activation");
#endif
snprintf (port_str, sizeof port_str, "%d", port);
if (asprintf (&file_str, "file=%s", device) == -1)
error (EXIT_FAILURE, errno, "asprintf");
pid = fork ();
if (pid == -1) {
set_nbd_error ("fork: %m");
return 0;
}
if (pid == 0) { /* Child. */
close (0);
if (open ("/dev/null", O_RDONLY) == -1) {
perror ("open: /dev/null");
_exit (EXIT_FAILURE);
}
if (fds == NULL) { /* without socket activation */
execlp ("nbdkit",
"nbdkit",
"-r", /* readonly (vital!) */
"-p", port_str, /* listening port */
"-i", ipaddr, /* listen only on loopback interface */
"-f", /* don't fork */
"file", /* file plugin */
file_str, /* a device like file=/dev/sda */
NULL);
perror ("nbdkit");
_exit (EXIT_FAILURE);
}
else { /* socket activation */
socket_activation (fds, nr_fds);
execlp ("nbdkit",
"nbdkit",
"-r", /* readonly (vital!) */
"-f", /* don't fork */
"file", /* file plugin */
file_str, /* a device like file=/dev/sda */
NULL);
perror ("nbdkit");
_exit (EXIT_FAILURE);
}
}
/* Parent. */
return pid;
}
/**
* This is used when we are starting an NBD server that does not
* support socket activation. We have to pass the '-p' option to
* the NBD server, but there's no good way to choose a free port,
* so we have to just guess.
*
* Returns the port number on success or C<-1> on error.
*/
static int
get_local_port (void)
{
int port = nbd_local_port;
nbd_local_port++;
return port;
}
/**
* This is used when we are starting an NBD server which supports
* socket activation. We can open a listening socket on an unused
* local port and return it.
*
* Returns the port number on success or C<-1> on error.
*
* The file descriptor(s) bound are returned in the array *fds, *nr_fds.
* The caller must free the array.
*/
static int
open_listening_socket (const char *ipaddr, int **fds, size_t *nr_fds)
{
int port;
char port_str[16];
/* This just ensures we don't try the port we previously bound to. */
port = nbd_local_port;
/* Search for a free port. */
for (; port < 60000; ++port) {
snprintf (port_str, sizeof port_str, "%d", port);
if (bind_tcpip_socket (ipaddr, port_str, fds, nr_fds) == 0) {
/* See above. */
nbd_local_port = port + 1;
return port;
}
}
set_nbd_error ("cannot find a free local port");
return -1;
}
static int
bind_tcpip_socket (const char *ipaddr, const char *port,
int **fds_rtn, size_t *nr_fds_rtn)
{
struct addrinfo *ai = NULL;
struct addrinfo hints;
struct addrinfo *a;
int err;
int *fds = NULL;
size_t nr_fds;
int addr_in_use = 0;
memset (&hints, 0, sizeof hints);
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
hints.ai_socktype = SOCK_STREAM;
err = getaddrinfo (ipaddr, port, &hints, &ai);
if (err != 0) {
#if DEBUG_STDERR
fprintf (stderr, "%s: getaddrinfo: %s: %s: %s",
getprogname (), ipaddr ? ipaddr : "<any>", port,
gai_strerror (err));
#endif
return -1;
}
nr_fds = 0;
for (a = ai; a != NULL; a = a->ai_next) {
int sock, opt;
sock = socket (a->ai_family, a->ai_socktype, a->ai_protocol);
if (sock == -1)
error (EXIT_FAILURE, errno, "socket");
opt = 1;
if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) == -1)
perror ("setsockopt: SO_REUSEADDR");
#ifdef IPV6_V6ONLY
if (a->ai_family == PF_INET6) {
if (setsockopt (sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof opt) == -1)
perror ("setsockopt: IPv6 only");
}
#endif
if (bind (sock, a->ai_addr, a->ai_addrlen) == -1) {
if (errno == EADDRINUSE) {
addr_in_use = 1;
close (sock);
continue;
}
perror ("bind");
close (sock);
continue;
}
if (listen (sock, SOMAXCONN) == -1) {
perror ("listen");
close (sock);
continue;
}
nr_fds++;
fds = realloc (fds, sizeof (int) * nr_fds);
if (!fds)
error (EXIT_FAILURE, errno, "realloc");
fds[nr_fds-1] = sock;
}
freeaddrinfo (ai);
if (nr_fds == 0 && addr_in_use) {
#if DEBUG_STDERR
fprintf (stderr, "%s: unable to bind to %s:%s: %s\n",
getprogname (), ipaddr ? ipaddr : "<any>", port,
strerror (EADDRINUSE));
#endif
return -1;
}
#if DEBUG_STDERR
fprintf (stderr, "%s: bound to IP address %s:%s (%zu socket(s))\n",
getprogname (), ipaddr ? ipaddr : "<any>", port, nr_fds);
#endif
*fds_rtn = fds;
*nr_fds_rtn = nr_fds;
return 0;
}
/**
* Wait for a local NBD server to start and be listening for
* connections.
*/
int
wait_for_nbd_server_to_start (const char *ipaddr, int port)
{
int sockfd = -1;
int result = -1;
time_t start_t, now_t;
struct timespec half_sec = { .tv_sec = 0, .tv_nsec = 500000000 };
struct timeval timeout = { .tv_usec = 0 };
char magic[8]; /* NBDMAGIC */
size_t bytes_read = 0;
ssize_t recvd;
time (&start_t);
for (;;) {
time (&now_t);
if (now_t - start_t >= WAIT_NBD_TIMEOUT) {
set_nbd_error ("timed out waiting for NBD server to start");
goto cleanup;
}
/* Source port for probing NBD server should be one greater than
* port. It's not guaranteed to always bind to this port, but it
* will hint the kernel to start there and try incrementally
* higher ports if needed. This avoids the case where the kernel
* selects port as our source port, and we immediately connect to
* ourself. See:
* https://bugzilla.redhat.com/show_bug.cgi?id=1167774#c9
*/
sockfd = connect_with_source_port (ipaddr, port, port+1);
if (sockfd >= 0)
break;
nanosleep (&half_sec, NULL);
}
time (&now_t);
timeout.tv_sec = (start_t + WAIT_NBD_TIMEOUT) - now_t;
if (setsockopt (sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout) == -1) {
set_nbd_error ("waiting for NBD server to start: "
"setsockopt(SO_RCVTIMEO): %m");
goto cleanup;
}
do {
recvd = recv (sockfd, magic, sizeof magic - bytes_read, 0);
if (recvd == -1) {
set_nbd_error ("waiting for NBD server to start: recv: %m");
goto cleanup;
}
bytes_read += recvd;
} while (bytes_read < sizeof magic);
if (memcmp (magic, "NBDMAGIC", sizeof magic) != 0) {
set_nbd_error ("waiting for NBD server to start: "
"'NBDMAGIC' was not received from NBD server");
goto cleanup;
}
result = 0;
cleanup:
if (sockfd >= 0)
close (sockfd);
return result;
}
/**
* Connect to C<hostname:dest_port>, resolving the address using
* L<getaddrinfo(3)>.
*
* This also sets the source port of the connection to the first free
* port number E<ge> C<source_port>.
*
* This may involve multiple connections - to IPv4 and IPv6 for
* instance.
*/
static int
connect_with_source_port (const char *hostname, int dest_port, int source_port)
{
struct addrinfo hints;
struct addrinfo *results, *rp;
char dest_port_str[16];
int r, sockfd = -1;
int reuseaddr = 1;
snprintf (dest_port_str, sizeof dest_port_str, "%d", dest_port);
memset (&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; /* allow IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV; /* numeric dest port number */
hints.ai_protocol = 0; /* any protocol */
r = getaddrinfo (hostname, dest_port_str, &hints, &results);
if (r != 0) {
set_nbd_error ("getaddrinfo: %s/%s: %s",
hostname, dest_port_str, gai_strerror (r));
return -1;
}
for (rp = results; rp != NULL; rp = rp->ai_next) {
sockfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sockfd == -1)
continue;
/* If we run p2v repeatedly (say, running the tests in a loop),
* there's a decent chance we'll end up trying to bind() to a port
* that is in TIME_WAIT from a prior run. Handle that gracefully
* with SO_REUSEADDR.
*/
if (setsockopt (sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuseaddr, sizeof reuseaddr) == -1)
perror ("warning: setsockopt");
/* Need to bind the source port. */
if (bind_source_port (sockfd, rp->ai_family, source_port) == -1) {
close (sockfd);
sockfd = -1;
continue;
}
/* Connect. */
if (connect (sockfd, rp->ai_addr, rp->ai_addrlen) == -1) {
set_nbd_error ("waiting for NBD server to start: "
"connect to %s/%s: %m",
hostname, dest_port_str);
close (sockfd);
sockfd = -1;
continue;
}
break;
}
freeaddrinfo (results);
return sockfd;
}
static int
bind_source_port (int sockfd, int family, int source_port)
{
struct addrinfo hints;
struct addrinfo *results, *rp;
char source_port_str[16];
int r;
snprintf (source_port_str, sizeof source_port_str, "%d", source_port);
memset (&hints, 0, sizeof (hints));
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; /* numeric port number */
hints.ai_protocol = 0; /* any protocol */
r = getaddrinfo ("localhost", source_port_str, &hints, &results);
if (r != 0) {
set_nbd_error ("getaddrinfo (bind): localhost/%s: %s",
source_port_str, gai_strerror (r));
return -1;
}
for (rp = results; rp != NULL; rp = rp->ai_next) {
if (bind (sockfd, rp->ai_addr, rp->ai_addrlen) == 0)
goto bound;
}
set_nbd_error ("waiting for NBD server to start: "
"bind to source port %d: %m",
source_port);
freeaddrinfo (results);
return -1;
bound:
freeaddrinfo (results);
return 0;
}