blob: ed82b8f2533dcab2f0abea720e8a5649f5c9bf22 [file] [log] [blame]
/*
* Send NSM notify calls to all hosts listed in /var/lib/sm
*
* Copyright (C) 2004-2006 Olaf Kirch <okir@suse.de>
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <err.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <poll.h>
#include <sys/param.h>
#include <sys/syslog.h>
#include <arpa/inet.h>
#include <dirent.h>
#include <time.h>
#include <stdio.h>
#include <getopt.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>
#include <netdb.h>
#include <errno.h>
#include <grp.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include "conffile.h"
#include "sockaddr.h"
#include "xlog.h"
#include "nsm.h"
#include "nfslib.h"
#include "nfsrpc.h"
/* glibc before 2.3.4 */
#ifndef AI_NUMERICSERV
#define AI_NUMERICSERV 0
#endif
#define NSM_TIMEOUT 2
#define NSM_MAX_TIMEOUT 120 /* don't make this too big */
#define NLM_END_GRACE_FILE "/proc/fs/lockd/nlm_end_grace"
int lift_grace = 1;
int force = 0;
struct nsm_host {
struct nsm_host * next;
char * name;
const char * mon_name;
const char * my_name;
char * notify_arg;
struct addrinfo *ai;
time_t last_used;
time_t send_next;
unsigned int timeout;
unsigned int retries;
uint32_t xid;
};
static char nsm_hostname[SM_MAXSTRLEN + 1];
static int nsm_state;
static int nsm_family = AF_INET;
static int opt_debug = 0;
static _Bool opt_update_state = true;
static unsigned int opt_max_retry = 15 * 60;
static char * opt_srcaddr = NULL;
static char * opt_srcport = NULL;
static void notify(const int sock);
static int notify_host(int, struct nsm_host *);
static void recv_reply(int);
static void insert_host(struct nsm_host *);
static struct nsm_host *find_host(uint32_t);
static int record_pid(void);
static struct nsm_host * hosts = NULL;
__attribute__((__malloc__))
static struct addrinfo *
smn_lookup(const char *name)
{
struct addrinfo *ai = NULL;
struct addrinfo hint = {
.ai_family = (nsm_family == AF_INET ? AF_INET: AF_UNSPEC),
.ai_protocol = (int)IPPROTO_UDP,
};
int error;
res_init();
error = getaddrinfo(name, NULL, &hint, &ai);
if (error != 0) {
xlog(D_GENERAL, "getaddrinfo(3): %s", gai_strerror(error));
return NULL;
}
return ai;
}
#ifdef HAVE_GETNAMEINFO
static char *
smn_get_hostname(const struct sockaddr *sap, const socklen_t salen,
const char *name)
{
char buf[NI_MAXHOST];
int error;
error = getnameinfo(sap, salen, buf, sizeof(buf), NULL, 0, NI_NAMEREQD);
if (error != 0) {
xlog(L_ERROR, "my_name '%s' is unusable: %s",
name, gai_strerror(error));
return NULL;
}
return strdup(buf);
}
#else /* !HAVE_GETNAMEINFO */
static char *
smn_get_hostname(const struct sockaddr *sap,
__attribute__ ((unused)) const socklen_t salen,
const char *name)
{
const struct sockaddr_in *sin = (const struct sockaddr_in *)(char *)sap;
const struct in_addr *addr = &sin->sin_addr;
struct hostent *hp;
if (sap->sa_family != AF_INET) {
xlog(L_ERROR, "my_name '%s' is unusable: Bad address family",
name);
return NULL;
}
hp = gethostbyaddr(addr, (socklen_t)sizeof(addr), AF_INET);
if (hp == NULL) {
xlog(L_ERROR, "my_name '%s' is unusable: %s",
name, hstrerror(h_errno));
return NULL;
}
return strdup(hp->h_name);
}
#endif /* !HAVE_GETNAMEINFO */
/*
* Presentation addresses are converted to their canonical hostnames.
* If the IP address does not map to a hostname, it is an error:
* we never send a presentation address as the argument of SM_NOTIFY.
*
* If "name" is not a presentation address, it is left alone. This
* allows the administrator some flexibility if DNS isn't configured
* exactly how sm-notify prefers it.
*
* Returns NUL-terminated C string containing the result, or NULL
* if the canonical name doesn't exist or cannot be determined.
* The caller must free the result with free(3).
*/
__attribute__((__malloc__))
static char *
smn_verify_my_name(const char *name)
{
struct addrinfo *ai = NULL;
struct addrinfo hint = {
#ifdef IPV6_SUPPORTED
.ai_family = AF_UNSPEC,
#else /* !IPV6_SUPPORTED */
.ai_family = AF_INET,
#endif /* !IPV6_SUPPORTED */
.ai_flags = AI_NUMERICHOST,
};
char *retval;
int error;
error = getaddrinfo(name, NULL, &hint, &ai);
switch (error) {
case 0:
/* @name was a presentation address */
retval = smn_get_hostname(ai->ai_addr, ai->ai_addrlen, name);
nfs_freeaddrinfo(ai);
if (retval == NULL)
return NULL;
break;
case EAI_NONAME:
/* @name was not a presentation address */
retval = strdup(name);
break;
default:
xlog(L_ERROR, "my_name '%s' is unusable: %s",
name, gai_strerror(error));
return NULL;
}
xlog(D_GENERAL, "Canonical name for my_name '%s': %s",
name, retval);
return retval;
}
__attribute__((__malloc__))
static struct nsm_host *
smn_alloc_host(const char *hostname, const char *mon_name,
const char *my_name, const time_t timestamp)
{
struct nsm_host *host;
host = calloc(1, sizeof(*host));
if (host == NULL)
goto out_nomem;
/*
* mon_name and my_name are preserved so sm-notify can
* find the right monitor record to remove when it is
* done processing this host.
*/
host->name = strdup(hostname);
host->mon_name = (const char *)strdup(mon_name);
host->my_name = (const char *)strdup(my_name);
host->notify_arg = strdup(opt_srcaddr != NULL ?
nsm_hostname : my_name);
if (host->name == NULL ||
host->mon_name == NULL ||
host->my_name == NULL ||
host->notify_arg == NULL) {
free(host->notify_arg);
free((void *)host->my_name);
free((void *)host->mon_name);
free(host->name);
free(host);
goto out_nomem;
}
host->last_used = timestamp;
host->timeout = NSM_TIMEOUT;
host->retries = 100; /* force address retry */
return host;
out_nomem:
xlog_warn("Unable to allocate memory");
return NULL;
}
static void smn_forget_host(struct nsm_host *host)
{
xlog(D_CALL, "Removing %s (%s, %s) from notify list",
host->name, host->mon_name, host->my_name);
nsm_delete_notified_host(host->name, host->mon_name, host->my_name);
free(host->notify_arg);
free((void *)host->my_name);
free((void *)host->mon_name);
free(host->name);
nfs_freeaddrinfo(host->ai);
free(host);
}
static unsigned int
smn_get_host(const char *hostname,
__attribute__ ((unused)) const struct sockaddr *sap,
const struct mon *m, const time_t timestamp)
{
struct nsm_host *host;
host = smn_alloc_host(hostname,
m->mon_id.mon_name, m->mon_id.my_id.my_name, timestamp);
if (host == NULL)
return 0;
insert_host(host);
return 1;
}
#ifdef IPV6_SUPPORTED
static int smn_socket(void)
{
int sock;
/*
* Use an AF_INET socket if IPv6 is disabled on the
* local system.
*/
sock = socket(AF_INET6, SOCK_DGRAM, 0);
if (sock == -1) {
if (errno != EAFNOSUPPORT) {
xlog(L_ERROR, "Failed to create RPC socket: %m");
return -1;
}
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
xlog(L_ERROR, "Failed to create RPC socket: %m");
return -1;
}
} else
nsm_family = AF_INET6;
if (fcntl(sock, F_SETFL, O_NONBLOCK) == -1) {
xlog(L_ERROR, "fcntl(3) on RPC socket failed: %m");
goto out_close;
}
/*
* TI-RPC over IPv6 (udp6/tcp6) does not handle IPv4. However,
* since sm-notify open-codes all of its RPC support, it can
* use a single socket and let the local network stack provide
* the correct mapping between address families automatically.
* This is the same thing that is done in the kernel.
*/
if (nsm_family == AF_INET6) {
const int zero = 0;
socklen_t zerolen = (socklen_t)sizeof(zero);
if (setsockopt(sock, SOL_IPV6, IPV6_V6ONLY,
(char *)&zero, zerolen) == -1) {
xlog(L_ERROR, "setsockopt(3) on RPC socket failed: %m");
goto out_close;
}
}
return sock;
out_close:
(void)close(sock);
return -1;
}
#else /* !IPV6_SUPPORTED */
static int smn_socket(void)
{
int sock;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock == -1) {
xlog(L_ERROR, "Failed to create RPC socket: %m");
return -1;
}
if (fcntl(sock, F_SETFL, O_NONBLOCK) == -1) {
xlog(L_ERROR, "fcntl(3) on RPC socket failed: %m");
(void)close(sock);
return -1;
}
return sock;
}
#endif /* !IPV6_SUPPORTED */
/*
* If admin specified a source address or srcport, then convert those
* to a sockaddr and return it. Otherwise, return an ANYADDR address.
*/
__attribute__((__malloc__))
static struct addrinfo *
smn_bind_address(const char *srcaddr, const char *srcport)
{
struct addrinfo *ai = NULL;
struct addrinfo hint = {
.ai_flags = AI_NUMERICSERV | AI_V4MAPPED,
.ai_family = nsm_family,
.ai_protocol = (int)IPPROTO_UDP,
};
int error;
if (srcaddr == NULL)
hint.ai_flags |= AI_PASSIVE;
/* Do not allow "node" and "service" parameters both to be NULL */
if (srcport == NULL)
error = getaddrinfo(srcaddr, "", &hint, &ai);
else
error = getaddrinfo(srcaddr, srcport, &hint, &ai);
if (error != 0) {
xlog(L_ERROR,
"Invalid bind address or port for RPC socket: %s",
gai_strerror(error));
return NULL;
}
return ai;
}
#ifdef HAVE_LIBTIRPC
static int
smn_bindresvport(int sock, struct sockaddr *sap)
{
return bindresvport_sa(sock, sap);
}
#else /* !HAVE_LIBTIRPC */
static int
smn_bindresvport(int sock, struct sockaddr *sap)
{
if (sap->sa_family != AF_INET) {
errno = EAFNOSUPPORT;
return -1;
}
return bindresvport(sock, (struct sockaddr_in *)(char *)sap);
}
#endif /* !HAVE_LIBTIRPC */
/*
* Prepare a socket for sending RPC requests
*
* Returns a bound datagram socket file descriptor, or -1 if
* an error occurs.
*/
static int
smn_create_socket(const char *srcaddr, const char *srcport)
{
int sock, retry_cnt = 0;
struct addrinfo *ai;
retry:
sock = smn_socket();
if (sock == -1)
return -1;
ai = smn_bind_address(srcaddr, srcport);
if (ai == NULL) {
(void)close(sock);
return -1;
}
/* Use source port if provided on the command line,
* otherwise use bindresvport */
if (srcport) {
if (bind(sock, ai->ai_addr, ai->ai_addrlen) == -1) {
xlog(L_ERROR, "Failed to bind RPC socket: %m");
nfs_freeaddrinfo(ai);
(void)close(sock);
return -1;
}
} else {
struct servent *se;
if (smn_bindresvport(sock, ai->ai_addr) == -1) {
xlog(L_ERROR,
"bindresvport on RPC socket failed: %m");
nfs_freeaddrinfo(ai);
(void)close(sock);
return -1;
}
/* try to avoid known ports */
se = getservbyport((int)nfs_get_port(ai->ai_addr), "udp");
if (se != NULL && retry_cnt < 100) {
retry_cnt++;
nfs_freeaddrinfo(ai);
(void)close(sock);
goto retry;
}
}
nfs_freeaddrinfo(ai);
return sock;
}
/* Inform the kernel that it's OK to lift lockd's grace period */
static void
nsm_lift_grace_period(void)
{
int fd;
fd = open(NLM_END_GRACE_FILE, O_WRONLY);
if (fd < 0) {
/* Don't warn if file isn't present */
if (errno != ENOENT)
xlog(L_WARNING, "Unable to open %s: %m",
NLM_END_GRACE_FILE);
return;
}
if (write(fd, "Y", 1) < 0)
xlog(L_WARNING, "Unable to write to %s: %m", NLM_END_GRACE_FILE);
close(fd);
return;
}
inline static void
read_smnotify_conf(char **argv)
{
char *s;
conf_init_file(NFS_CONFFILE);
xlog_set_debug("sm-notify");
opt_max_retry = conf_get_num("sm-notify", "retry-time", opt_max_retry / 60) * 60;
opt_srcport = conf_get_str("sm-notify", "outgoing-port");
opt_srcaddr = conf_get_str("sm-notify", "outgoing-addr");
lift_grace = conf_get_bool("sm-notify", "lift-grace", lift_grace);
s = conf_get_str("statd", "state-directory-path");
if (s && !nsm_setup_pathnames(argv[0], s))
exit(1);
opt_update_state = conf_get_bool("sm-notify", "update-state", opt_update_state);
force = conf_get_bool("sm-notify", "force", force);
}
int
main(int argc, char **argv)
{
int c, sock;
char * progname;
progname = strrchr(argv[0], '/');
if (progname != NULL)
progname++;
else
progname = argv[0];
/* Read in config setting */
read_smnotify_conf(argv);
while ((c = getopt(argc, argv, "dm:np:v:P:f")) != -1) {
switch (c) {
case 'f':
force = 1;
break;
case 'd':
opt_debug++;
break;
case 'm':
opt_max_retry = atoi(optarg) * 60;
break;
case 'n':
opt_update_state = false;
break;
case 'p':
opt_srcport = optarg;
break;
case 'v':
opt_srcaddr = optarg;
break;
case 'P':
if (!nsm_setup_pathnames(argv[0], optarg))
exit(1);
break;
default:
goto usage;
}
}
if (optind < argc) {
usage: fprintf(stderr,
"Usage: %s -notify [-dfq] [-m max-retry-minutes] [-p srcport]\n"
" [-P /path/to/state/directory] [-v my_host_name]\n",
progname);
exit(1);
}
if (opt_debug) {
xlog_syslog(0);
xlog_stderr(1);
xlog_config(D_ALL, 1);
} else {
xlog_syslog(1);
xlog_stderr(0);
}
xlog_open(progname);
xlog(L_NOTICE, "Version " VERSION " starting");
if (nsm_is_default_parentdir()) {
if (record_pid() == 0 && force == 0 && opt_update_state) {
/* already run, don't try again */
xlog(L_NOTICE, "Already notifying clients; Exiting!");
exit(0);
}
}
if (opt_srcaddr != NULL) {
char *name;
name = smn_verify_my_name(opt_srcaddr);
if (name == NULL)
exit(1);
strncpy(nsm_hostname, name, sizeof(nsm_hostname)-1);
free(name);
}
(void)nsm_retire_monitored_hosts();
if (nsm_load_notify_list(smn_get_host) == 0) {
xlog(D_GENERAL, "No hosts to notify; exiting");
if (lift_grace)
nsm_lift_grace_period();
return 0;
}
nsm_state = nsm_get_state(opt_update_state);
if (nsm_state == 0)
exit(1);
nsm_update_kernel_state(nsm_state);
if (!opt_debug) {
xlog(L_NOTICE, "Backgrounding to notify hosts...\n");
if (daemon(0, 0) < 0) {
xlog(L_ERROR, "unable to background: %m");
exit(1);
}
close(0);
close(1);
close(2);
}
sock = smn_create_socket(opt_srcaddr, opt_srcport);
if (sock == -1)
exit(1);
if (!nsm_drop_privileges(-1))
exit(1);
notify(sock);
if (hosts) {
struct nsm_host *hp;
while ((hp = hosts) != 0) {
hosts = hp->next;
xlog(L_NOTICE, "Unable to notify %s, giving up",
hp->name);
}
exit(1);
}
exit(0);
}
/*
* Notify hosts
*/
static void
notify(const int sock)
{
time_t failtime = 0;
if (opt_max_retry)
failtime = time(NULL) + opt_max_retry;
while (hosts) {
struct pollfd pfd;
time_t now = time(NULL);
unsigned int sent = 0;
struct nsm_host *hp;
long wait;
if (failtime && now >= failtime)
break;
while (hosts && ((wait = hosts->send_next - now) <= 0)) {
/* Never send more than 10 packets at once */
if (sent++ >= 10)
break;
/* Remove queue head */
hp = hosts;
hosts = hp->next;
if (notify_host(sock, hp))
continue;
/* Set the timeout for this call, using an
exponential timeout strategy */
wait = hp->timeout;
if ((hp->timeout <<= 1) > NSM_MAX_TIMEOUT)
hp->timeout = NSM_MAX_TIMEOUT;
hp->send_next = now + wait;
hp->retries++;
insert_host(hp);
}
if (hosts == NULL)
return;
xlog(D_GENERAL, "Host %s due in %ld seconds",
hosts->name, wait);
pfd.fd = sock;
pfd.events = POLLIN;
wait *= 1000;
if (wait < 100)
wait = 100;
if (poll(&pfd, 1, wait) != 1)
continue;
recv_reply(sock);
}
}
/*
* Send notification to a single host
*/
static int
notify_host(int sock, struct nsm_host *host)
{
struct sockaddr *sap;
socklen_t salen;
if (host->ai == NULL) {
host->ai = smn_lookup(host->name);
if (host->ai == NULL) {
xlog_warn("DNS resolution of %s failed; "
"retrying later", host->name);
return 0;
}
}
/* If we retransmitted 4 times, reset the port to force
* a new portmap lookup (in case statd was restarted).
* We also rotate through multiple IP addresses at this
* point.
*/
if (host->retries >= 4) {
/* don't rotate if there is only one addrinfo */
if (host->ai->ai_next != NULL) {
struct addrinfo *first = host->ai;
struct addrinfo **next = &host->ai;
/* remove the first entry from the list */
host->ai = first->ai_next;
first->ai_next = NULL;
/* find the end of the list */
next = &first->ai_next;
while ( *next )
next = & (*next)->ai_next;
/* put first entry at end */
*next = first;
}
nfs_set_port(host->ai->ai_addr, 0);
host->retries = 0;
}
sap = host->ai->ai_addr;
salen = host->ai->ai_addrlen;
if (nfs_get_port(sap) == 0)
host->xid = nsm_xmit_rpcbind(sock, sap, SM_PROG, SM_VERS);
else
host->xid = nsm_xmit_notify(sock, sap, salen,
SM_PROG, host->notify_arg, nsm_state);
return 0;
}
static void
smn_defer(struct nsm_host *host)
{
host->xid = 0;
host->send_next = time(NULL) + NSM_MAX_TIMEOUT;
host->timeout = NSM_MAX_TIMEOUT;
insert_host(host);
}
static void
smn_schedule(struct nsm_host *host)
{
host->retries = 0;
host->xid = 0;
host->send_next = time(NULL);
host->timeout = NSM_TIMEOUT;
insert_host(host);
}
/*
* Extract the returned port number and set up the SM_NOTIFY call.
*/
static void
recv_rpcbind_reply(struct sockaddr *sap, struct nsm_host *host, XDR *xdr)
{
uint16_t port = nsm_recv_rpcbind(sap->sa_family, xdr);
if (port == 0) {
/* No binding for statd... */
xlog(D_GENERAL, "No statd on host %s", host->name);
smn_defer(host);
} else {
xlog(D_GENERAL, "Processing rpcbind reply for %s (port %u)",
host->name, port);
nfs_set_port(sap, port);
smn_schedule(host);
}
}
/*
* Successful NOTIFY call. Server returns void.
*
* Try sending another SM_NOTIFY with an unqualified "my_name"
* argument. Reuse the port number. If "my_name" is already
* unqualified, we're done.
*/
static void
recv_notify_reply(struct nsm_host *host)
{
char *dot = strchr(host->notify_arg, '.');
if (dot != NULL) {
*dot = '\0';
smn_schedule(host);
} else {
xlog(D_GENERAL, "Host %s notified successfully", host->name);
smn_forget_host(host);
}
}
/*
* Receive reply from remote host
*/
static void
recv_reply(int sock)
{
struct nsm_host *hp;
struct sockaddr *sap;
char msgbuf[NSM_MAXMSGSIZE];
uint32_t xid;
ssize_t msglen;
XDR xdr;
memset(msgbuf, 0 , sizeof(msgbuf));
msglen = recv(sock, msgbuf, sizeof(msgbuf), 0);
if (msglen < 0)
return;
xlog(D_GENERAL, "Received packet...");
memset(&xdr, 0, sizeof(xdr));
xdrmem_create(&xdr, msgbuf, (unsigned int)msglen, XDR_DECODE);
xid = nsm_parse_reply(&xdr);
if (xid == 0)
goto out;
/* Before we look at the data, find the host struct for
this reply */
if ((hp = find_host(xid)) == NULL)
goto out;
sap = hp->ai->ai_addr;
if (nfs_get_port(sap) == 0)
recv_rpcbind_reply(sap, hp, &xdr);
else
recv_notify_reply(hp);
out:
xdr_destroy(&xdr);
}
/*
* Insert host into notification list, sorted by next send time
*/
static void
insert_host(struct nsm_host *host)
{
struct nsm_host **where, *p;
where = &hosts;
while ((p = *where) != 0) {
/* Sort in ascending order of timeout */
if (host->send_next < p->send_next)
break;
/* If we have the same timeout, put the
* most recently used host first.
* This makes sure that "recent" hosts
* get notified first.
*/
if (host->send_next == p->send_next
&& host->last_used > p->last_used)
break;
where = &p->next;
}
host->next = *where;
*where = host;
xlog(D_GENERAL, "Added host %s to notify list", host->name);
}
/*
* Find host given the XID
*/
static struct nsm_host *
find_host(uint32_t xid)
{
struct nsm_host **where, *p;
where = &hosts;
while ((p = *where) != 0) {
if (p->xid == xid) {
*where = p->next;
return p;
}
where = &p->next;
}
return NULL;
}
/*
* Record pid in /run/sm-notify.pid
* This file should remain until a reboot, even if the
* program exits.
* If file already exists, fail.
*/
static int record_pid(void)
{
char pid[20];
ssize_t len;
int fd;
(void)snprintf(pid, sizeof(pid), "%d\n", (int)getpid());
fd = open("/run/sm-notify.pid", O_CREAT|O_EXCL|O_WRONLY, 0600);
if (fd < 0)
return 0;
len = write(fd, pid, strlen(pid));
if ((len < 0) || ((size_t)len != strlen(pid))) {
xlog_warn("Writing to pid file failed: errno %d (%m)",
errno);
}
(void)close(fd);
return 1;
}