NetBSD/usr.sbin/ifwatchd/ifwatchd.c
roy 82328146fe ifwatchd: Enable SO_RERROR to re-sync interface state.
Interface arrival, departure and link state changes will sync
and if different will be actioned.

Currently we do not track addresses, so any changes there are still lost.
2020-10-04 20:36:32 +00:00

632 lines
15 KiB
C

/* $NetBSD: ifwatchd.c,v 1.46 2020/10/04 20:36:32 roy Exp $ */
#include <sys/cdefs.h>
__RCSID("$NetBSD: ifwatchd.c,v 1.46 2020/10/04 20:36:32 roy Exp $");
/*-
* Copyright (c) 2002, 2003 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Martin Husemann <martin@NetBSD.org>.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <sys/types.h>
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/queue.h>
#include <sys/wait.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/route.h>
#include <netinet/in.h>
#include <netinet/in_var.h>
#include <arpa/inet.h>
#include <err.h>
#include <errno.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <paths.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
enum event { ARRIVAL, DEPARTURE, UP, DOWN, CARRIER, NO_CARRIER };
enum addrflag { NOTREADY, DETACHED, READY };
/* local functions */
__dead static void usage(void);
static void dispatch(const void *, size_t);
static enum addrflag check_addrflags(int af, int addrflags);
static void check_addrs(const struct ifa_msghdr *ifam);
static void invoke_script(const char *ifname, enum event ev,
const struct sockaddr *sa, const struct sockaddr *dst);
static void list_interfaces(const char *ifnames);
static void check_announce(const struct if_announcemsghdr *ifan);
static void check_carrier(const struct if_msghdr *ifm);
static void free_interfaces(void);
static struct interface_data * find_interface(int index);
static void run_initial_ups(bool);
/* global variables */
static int verbose = 0, quiet = 0;
static int inhibit_initial = 0;
static const char *arrival_script = NULL;
static const char *departure_script = NULL;
static const char *up_script = NULL;
static const char *down_script = NULL;
static const char *carrier_script = NULL;
static const char *no_carrier_script = NULL;
static const char DummyTTY[] = _PATH_DEVNULL;
static const char DummySpeed[] = "9600";
static const char **scripts[] = {
&arrival_script,
&departure_script,
&up_script,
&down_script,
&carrier_script,
&no_carrier_script
};
struct interface_data {
SLIST_ENTRY(interface_data) next;
int index;
int last_carrier_status;
char * ifname;
};
static SLIST_HEAD(,interface_data) ifs = SLIST_HEAD_INITIALIZER(ifs);
int
main(int argc, char **argv)
{
int c, s, n;
int errs = 0;
struct msghdr msg;
struct iovec iov[1];
char buf[2048];
unsigned char msgfilter[] = {
RTM_IFINFO, RTM_IFANNOUNCE,
RTM_NEWADDR, RTM_DELADDR,
};
openlog(argv[0], LOG_PID|LOG_CONS, LOG_DAEMON);
while ((c = getopt(argc, argv, "qvhic:n:u:d:A:D:")) != -1) {
switch (c) {
case 'h':
usage();
return 0;
case 'i':
inhibit_initial = 1;
break;
case 'v':
verbose++;
break;
case 'q':
quiet = 1;
break;
case 'c':
carrier_script = optarg;
break;
case 'n':
no_carrier_script = optarg;
break;
case 'u':
up_script = optarg;
break;
case 'd':
down_script = optarg;
break;
case 'A':
arrival_script = optarg;
break;
case 'D':
departure_script = optarg;
break;
default:
errs++;
break;
}
}
if (errs)
usage();
argv += optind;
argc -= optind;
if (argc <= 0)
usage();
if (verbose) {
printf("up_script: %s\ndown_script: %s\n",
up_script, down_script);
printf("arrival_script: %s\ndeparture_script: %s\n",
arrival_script, departure_script);
printf("carrier_script: %s\nno_carrier_script: %s\n",
carrier_script, no_carrier_script);
printf("verbosity = %d\n", verbose);
}
while (argc > 0) {
list_interfaces(argv[0]);
argv++;
argc--;
}
if (!verbose)
daemon(0, 0);
s = socket(PF_ROUTE, SOCK_RAW, 0);
if (s < 0) {
syslog(LOG_ERR, "error opening routing socket: %m");
exit(EXIT_FAILURE);
}
if (setsockopt(s, PF_ROUTE, RO_MSGFILTER,
&msgfilter, sizeof(msgfilter)) < 0)
syslog(LOG_ERR, "RO_MSGFILTER: %m");
n = 1;
if (setsockopt(s, SOL_SOCKET, SO_RERROR, &n, sizeof(n)) < 0)
syslog(LOG_ERR, "SO_RERROR: %m");
if (!inhibit_initial)
run_initial_ups(true);
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = 1;
for (;;) {
n = recvmsg(s, &msg, 0);
if (n == -1) {
if (errno == ENOBUFS) {
syslog(LOG_ERR,
"routing socket overflow detected");
/* XXX We don't track addresses, so they
* won't be reported. */
if (!inhibit_initial)
run_initial_ups(false);
continue;
}
syslog(LOG_ERR, "recvmsg: %m");
exit(EXIT_FAILURE);
}
if (n != 0)
dispatch(iov[0].iov_base, n);
}
close(s);
free_interfaces();
closelog();
return EXIT_SUCCESS;
}
static void
usage(void)
{
fprintf(stderr,
"usage:\n"
"\tifwatchd [-hiqv] [-A arrival-script] [-D departure-script]\n"
"\t\t [-d down-script] [-u up-script]\n"
"\t\t [-c carrier-script] [-n no-carrier-script] ifname(s)\n"
"\twhere:\n"
"\t -A <cmd> specify command to run on interface arrival event\n"
"\t -c <cmd> specify command to run on interface carrier-detect event\n"
"\t -D <cmd> specify command to run on interface departure event\n"
"\t -d <cmd> specify command to run on interface down event\n"
"\t -n <cmd> specify command to run on interface no-carrier-detect event\n"
"\t -h show this help message\n"
"\t -i no (!) initial run of the up script if the interface\n"
"\t is already up on ifwatchd startup\n"
"\t -q quiet mode, don't syslog informational messages\n"
"\t -u <cmd> specify command to run on interface up event\n"
"\t -v verbose/debug output, don't run in background\n");
exit(EXIT_FAILURE);
}
static void
dispatch(const void *msg, size_t len)
{
const struct rt_msghdr *hd = msg;
if (hd->rtm_version != RTM_VERSION)
return;
switch (hd->rtm_type) {
case RTM_NEWADDR:
case RTM_DELADDR:
check_addrs(msg);
break;
case RTM_IFANNOUNCE:
check_announce(msg);
break;
case RTM_IFINFO:
check_carrier(msg);
break;
default:
/* Should be impossible as we filter messages. */
if (verbose)
printf("unknown message ignored (%d)\n", hd->rtm_type);
break;
}
}
static enum addrflag
check_addrflags(int af, int addrflags)
{
switch (af) {
case AF_INET:
if (addrflags & IN_IFF_NOTREADY)
return NOTREADY;
if (addrflags & IN_IFF_DETACHED)
return DETACHED;
break;
case AF_INET6:
if (addrflags & IN6_IFF_NOTREADY)
return NOTREADY;
if (addrflags & IN6_IFF_DETACHED)
return DETACHED;
break;
}
return READY;
}
static void
check_addrs(const struct ifa_msghdr *ifam)
{
const char *cp = (const char *)(ifam + 1);
const struct sockaddr *sa, *ifa = NULL, *brd = NULL;
unsigned i;
struct interface_data *ifd = NULL;
int aflag;
enum event ev;
if (ifam->ifam_addrs == 0)
return;
for (i = 1; i; i <<= 1) {
if ((i & ifam->ifam_addrs) == 0)
continue;
sa = (const struct sockaddr *)cp;
if (i == RTA_IFP) {
const struct sockaddr_dl *li;
li = (const struct sockaddr_dl *)sa;
if ((ifd = find_interface(li->sdl_index)) == NULL) {
if (verbose)
printf("ignoring change"
" on interface #%d\n",
li->sdl_index);
return;
}
} else if (i == RTA_IFA)
ifa = sa;
else if (i == RTA_BRD)
brd = sa;
RT_ADVANCE(cp, sa);
}
if (ifa != NULL && ifd != NULL) {
ev = ifam->ifam_type == RTM_DELADDR ? DOWN : UP;
aflag = check_addrflags(ifa->sa_family, ifam->ifam_addrflags);
if ((ev == UP && aflag == READY) || ev == DOWN)
invoke_script(ifd->ifname, ev, ifa, brd);
}
}
static void
invoke_script(const char *ifname, enum event ev,
const struct sockaddr *sa, const struct sockaddr *dest)
{
char addr[NI_MAXHOST], daddr[NI_MAXHOST];
const char *script;
int status;
if (ifname == NULL)
return;
script = *scripts[ev];
if (script == NULL)
return;
addr[0] = daddr[0] = 0;
if (sa != NULL) {
const struct sockaddr_in *sin;
const struct sockaddr_in6 *sin6;
if (sa->sa_len == 0) {
syslog(LOG_ERR,
"illegal socket address (sa_len == 0)");
return;
}
switch (sa->sa_family) {
case AF_INET:
sin = (const struct sockaddr_in *)sa;
if (sin->sin_addr.s_addr == INADDR_ANY ||
sin->sin_addr.s_addr == INADDR_BROADCAST)
return;
break;
case AF_INET6:
sin6 = (const struct sockaddr_in6 *)sa;
if (IN6_IS_ADDR_LINKLOCAL(&sin6->sin6_addr))
return;
break;
default:
break;
}
if (getnameinfo(sa, sa->sa_len, addr, sizeof addr, NULL, 0,
NI_NUMERICHOST)) {
if (verbose)
printf("getnameinfo failed\n");
return; /* this address can not be handled */
}
}
if (dest != NULL) {
if (getnameinfo(dest, dest->sa_len, daddr, sizeof daddr,
NULL, 0, NI_NUMERICHOST)) {
if (verbose)
printf("getnameinfo failed\n");
return; /* this address can not be handled */
}
}
if (verbose)
(void) printf("calling: %s %s %s %s %s %s\n",
script, ifname, DummyTTY, DummySpeed, addr, daddr);
if (!quiet)
syslog(LOG_INFO, "calling: %s %s %s %s %s %s\n",
script, ifname, DummyTTY, DummySpeed, addr, daddr);
switch (vfork()) {
case -1:
syslog(LOG_ERR, "cannot fork: %m");
break;
case 0:
if (execl(script, script, ifname, DummyTTY, DummySpeed,
addr, daddr, NULL) == -1) {
syslog(LOG_ERR, "could not execute \"%s\": %m",
script);
}
_exit(EXIT_FAILURE);
default:
(void) wait(&status);
}
}
static void
list_interfaces(const char *ifnames)
{
char * names = strdup(ifnames);
char * name, *lasts;
static const char sep[] = " \t";
struct interface_data * p;
for (name = strtok_r(names, sep, &lasts);
name != NULL;
name = strtok_r(NULL, sep, &lasts)) {
p = malloc(sizeof(*p));
SLIST_INSERT_HEAD(&ifs, p, next);
p->last_carrier_status = -1;
p->ifname = strdup(name);
p->index = if_nametoindex(p->ifname);
if (!quiet)
syslog(LOG_INFO, "watching interface %s", p->ifname);
if (verbose)
printf("interface \"%s\" has index %d\n",
p->ifname, p->index);
}
free(names);
}
static void
check_carrier(const struct if_msghdr *ifm)
{
struct interface_data * p;
int carrier_status;
enum event ev;
SLIST_FOREACH(p, &ifs, next)
if (p->index == ifm->ifm_index)
break;
if (p == NULL)
return;
/*
* Treat it as an event worth handling if:
* - the carrier status changed, or
* - this is the first time we've been called, and
* inhibit_initial is not set
*/
carrier_status = ifm->ifm_data.ifi_link_state;
if (carrier_status != p->last_carrier_status) {
switch (carrier_status) {
case LINK_STATE_UP:
ev = CARRIER;
break;
case LINK_STATE_DOWN:
ev = NO_CARRIER;
break;
default:
if (verbose)
printf("unknown link status ignored\n");
return;
}
invoke_script(p->ifname, ev, NULL, NULL);
p->last_carrier_status = carrier_status;
}
}
static void
do_announce(struct interface_data *ifd,
unsigned short what, unsigned short index)
{
switch (what) {
case IFAN_ARRIVAL:
ifd->index = index;
invoke_script(ifd->ifname, ARRIVAL, NULL, NULL);
break;
case IFAN_DEPARTURE:
ifd->index = -1;
ifd->last_carrier_status = -1;
invoke_script(ifd->ifname, DEPARTURE, NULL, NULL);
break;
default:
if (verbose)
(void) printf("unknown announce: what=%d\n", what);
break;
}
}
static void
check_announce(const struct if_announcemsghdr *ifan)
{
struct interface_data * p;
const char *ifname = ifan->ifan_name;
SLIST_FOREACH(p, &ifs, next) {
if (strcmp(p->ifname, ifname) != 0)
continue;
do_announce(p, ifan->ifan_what, ifan->ifan_index);
return;
}
}
static void
free_interfaces(void)
{
struct interface_data * p;
while (!SLIST_EMPTY(&ifs)) {
p = SLIST_FIRST(&ifs);
SLIST_REMOVE_HEAD(&ifs, next);
free(p->ifname);
free(p);
}
}
static struct interface_data *
find_interface(int idx)
{
struct interface_data * p;
SLIST_FOREACH(p, &ifs, next)
if (p->index == idx)
return p;
return NULL;
}
static void
run_initial_ups(bool do_addrs)
{
struct interface_data * ifd;
struct ifaddrs *res = NULL, *p;
struct sockaddr *ifa;
const struct if_data *ifi;
int s, aflag;
s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0)
return;
if (getifaddrs(&res) != 0)
goto out;
/* Check if any interfaces vanished */
SLIST_FOREACH(ifd, &ifs, next) {
for (p = res; p; p = p->ifa_next) {
if (strcmp(ifd->ifname, p->ifa_name) != 0)
continue;
ifa = p->ifa_addr;
if (ifa != NULL && ifa->sa_family == AF_LINK)
break;
}
if (p == NULL)
do_announce(ifd, IFAN_DEPARTURE, ifd->index);
}
for (p = res; p; p = p->ifa_next) {
SLIST_FOREACH(ifd, &ifs, next) {
if (strcmp(ifd->ifname, p->ifa_name) == 0)
break;
}
if (ifd == NULL)
continue;
ifa = p->ifa_addr;
if (ifa != NULL && ifa->sa_family == AF_LINK &&
ifd->index == -1)
invoke_script(ifd->ifname, ARRIVAL, NULL, NULL);
if ((p->ifa_flags & IFF_UP) == 0)
continue;
if (ifa == NULL)
continue;
if (ifa->sa_family == AF_LINK) {
ifi = (const struct if_data *)p->ifa_data;
if (ifd->last_carrier_status == ifi->ifi_link_state)
continue;
switch (ifi->ifi_link_state) {
case LINK_STATE_UP:
invoke_script(ifd->ifname, CARRIER, NULL, NULL);
break;
case LINK_STATE_DOWN:
if (ifd->last_carrier_status == -1)
break;
invoke_script(ifd->ifname, CARRIER, NULL, NULL);
break;
}
ifd->last_carrier_status = ifi->ifi_link_state;
continue;
}
if (!do_addrs)
continue;
aflag = check_addrflags(ifa->sa_family, p->ifa_addrflags);
if (aflag != READY)
continue;
invoke_script(ifd->ifname, UP, ifa, p->ifa_dstaddr);
}
freeifaddrs(res);
out:
close(s);
}