src/usr.sbin/ifstated/ifstated.c

752 lines
18 KiB
C

/* $OpenBSD: ifstated.c,v 1.68 2024/04/23 13:34:51 jsg Exp $ */
/*
* Copyright (c) 2004 Marco Pfatschbacher <mpf@openbsd.org>
* Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* ifstated listens to link_state transitions on interfaces
* and executes predefined commands.
*/
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <net/if.h>
#include <net/route.h>
#include <netinet/in.h>
#include <paths.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>
#include <syslog.h>
#include <errno.h>
#include <event.h>
#include <unistd.h>
#include <ifaddrs.h>
#include "ifstated.h"
#include "log.h"
struct ifsd_config *conf, *newconf;
int opts;
int opt_inhibit;
char *configfile = "/etc/ifstated.conf";
struct event rt_msg_ev, sighup_ev, startup_ev, sigchld_ev;
void startup_handler(int, short, void *);
void sighup_handler(int, short, void *);
int load_config(void);
void sigchld_handler(int, short, void *);
void rt_msg_handler(int, short, void *);
void external_handler(int, short, void *);
void external_exec(struct ifsd_external *, int);
void check_external_status(struct ifsd_state *);
void check_ifdeparture(void);
void external_evtimer_setup(struct ifsd_state *, int);
void scan_ifstate(const char *, int, int);
int scan_ifstate_single(const char *, int, struct ifsd_state *);
void fetch_ifstate(int);
__dead void usage(void);
void adjust_expressions(struct ifsd_expression_list *, int);
void adjust_external_expressions(struct ifsd_state *);
void eval_state(struct ifsd_state *);
int state_change(void);
void do_action(struct ifsd_action *);
void remove_action(struct ifsd_action *, struct ifsd_state *);
void remove_expression(struct ifsd_expression *,
struct ifsd_state *);
__dead void
usage(void)
{
extern char *__progname;
fprintf(stderr, "usage: %s [-dhinv] [-D macro=value] [-f file]\n",
__progname);
exit(1);
}
int
main(int argc, char *argv[])
{
struct timeval tv;
int ch, rt_fd;
int debug = 0;
unsigned int rtfilter;
log_init(1, LOG_DAEMON); /* log to stderr until daemonized */
log_setverbose(1);
while ((ch = getopt(argc, argv, "dD:f:hniv")) != -1) {
switch (ch) {
case 'd':
debug = 1;
break;
case 'D':
if (cmdline_symset(optarg) < 0)
fatalx("could not parse macro definition %s",
optarg);
break;
case 'f':
configfile = optarg;
break;
case 'h':
usage();
break;
case 'n':
opts |= IFSD_OPT_NOACTION;
break;
case 'i':
opt_inhibit = 1;
break;
case 'v':
if (opts & IFSD_OPT_VERBOSE)
opts |= IFSD_OPT_VERBOSE2;
opts |= IFSD_OPT_VERBOSE;
break;
default:
usage();
}
}
argc -= optind;
argv += optind;
if (argc > 0)
usage();
if (opts & IFSD_OPT_NOACTION) {
if ((newconf = parse_config(configfile, opts)) == NULL)
exit(1);
fprintf(stderr, "configuration OK\n");
exit(0);
}
if (!debug)
daemon(1, 0);
event_init();
log_init(debug, LOG_DAEMON);
log_setverbose(opts & IFSD_OPT_VERBOSE);
if ((rt_fd = socket(AF_ROUTE, SOCK_RAW, 0)) == -1)
fatal("no routing socket");
rtfilter = ROUTE_FILTER(RTM_IFINFO) | ROUTE_FILTER(RTM_IFANNOUNCE);
if (setsockopt(rt_fd, AF_ROUTE, ROUTE_MSGFILTER,
&rtfilter, sizeof(rtfilter)) == -1) /* not fatal */
log_warn("%s: setsockopt msgfilter", __func__);
rtfilter = RTABLE_ANY;
if (setsockopt(rt_fd, AF_ROUTE, ROUTE_TABLEFILTER,
&rtfilter, sizeof(rtfilter)) == -1) /* not fatal */
log_warn("%s: setsockopt tablefilter", __func__);
if (unveil(configfile, "r") == -1)
fatal("unveil %s", configfile);
if (unveil(_PATH_BSHELL, "x") == -1)
fatal("unveil %s", _PATH_BSHELL);
if (pledge("stdio rpath route proc exec", NULL) == -1)
fatal("pledge");
signal_set(&sigchld_ev, SIGCHLD, sigchld_handler, NULL);
signal_add(&sigchld_ev, NULL);
/* Loading the config needs to happen in the event loop */
timerclear(&tv);
evtimer_set(&startup_ev, startup_handler, (void *)(long)rt_fd);
evtimer_add(&startup_ev, &tv);
event_loop(0);
exit(0);
}
void
startup_handler(int fd, short event, void *arg)
{
int rfd = (int)(long)arg;
if (load_config() != 0) {
log_warnx("unable to load config");
exit(1);
}
event_set(&rt_msg_ev, rfd, EV_READ|EV_PERSIST, rt_msg_handler, NULL);
event_add(&rt_msg_ev, NULL);
signal_set(&sighup_ev, SIGHUP, sighup_handler, NULL);
signal_add(&sighup_ev, NULL);
log_info("started");
}
void
sighup_handler(int fd, short event, void *arg)
{
log_info("reloading config");
if (load_config() != 0)
log_warnx("unable to reload config");
}
int
load_config(void)
{
if ((newconf = parse_config(configfile, opts)) == NULL)
return (-1);
if (conf != NULL)
clear_config(conf);
conf = newconf;
conf->initstate.entered = time(NULL);
fetch_ifstate(0);
external_evtimer_setup(&conf->initstate, IFSD_EVTIMER_ADD);
adjust_external_expressions(&conf->initstate);
eval_state(&conf->initstate);
if (conf->curstate != NULL) {
log_info("initial state: %s", conf->curstate->name);
conf->curstate->entered = time(NULL);
conf->nextstate = conf->curstate;
conf->curstate = NULL;
while (state_change()) {
do_action(conf->curstate->init);
do_action(conf->curstate->body);
}
}
return (0);
}
void
rt_msg_handler(int fd, short event, void *arg)
{
char msg[2048];
struct rt_msghdr *rtm = (struct rt_msghdr *)&msg;
struct if_msghdr ifm;
struct if_announcemsghdr ifan;
char ifnamebuf[IFNAMSIZ];
char *ifname;
ssize_t len;
if ((len = read(fd, msg, sizeof(msg))) == -1) {
if (errno == EAGAIN || errno == EINTR)
return;
fatal("%s: routing socket read error", __func__);
}
if (len == 0)
fatal("%s: routing socket closed", __func__);
if (rtm->rtm_version != RTM_VERSION)
return;
switch (rtm->rtm_type) {
case RTM_IFINFO:
memcpy(&ifm, rtm, sizeof(ifm));
ifname = if_indextoname(ifm.ifm_index, ifnamebuf);
/* ifname is NULL on interface departure */
if (ifname != NULL)
scan_ifstate(ifname, ifm.ifm_data.ifi_link_state, 1);
break;
case RTM_IFANNOUNCE:
memcpy(&ifan, rtm, sizeof(ifan));
switch (ifan.ifan_what) {
case IFAN_DEPARTURE:
log_warnx("interface %s departed", ifan.ifan_name);
check_ifdeparture();
break;
case IFAN_ARRIVAL:
log_warnx("interface %s arrived", ifan.ifan_name);
fetch_ifstate(1);
break;
}
break;
case RTM_DESYNC:
/* we lost some routing messages so rescan interfaces */
check_ifdeparture();
fetch_ifstate(1);
break;
}
return;
}
void
sigchld_handler(int fd, short event, void *arg)
{
check_external_status(&conf->initstate);
if (conf->curstate != NULL)
check_external_status(conf->curstate);
}
void
external_handler(int fd, short event, void *arg)
{
struct ifsd_external *external = (struct ifsd_external *)arg;
struct timeval tv;
/* re-schedule */
timerclear(&tv);
tv.tv_sec = external->frequency;
evtimer_set(&external->ev, external_handler, external);
evtimer_add(&external->ev, &tv);
/* execute */
external_exec(external, 1);
}
void
external_exec(struct ifsd_external *external, int async)
{
char *argp[] = {"sh", "-c", NULL, NULL};
pid_t pid;
int s;
if (external->pid > 0) {
log_debug("previous command %s [%d] still running, killing it",
external->command, external->pid);
kill(external->pid, SIGKILL);
waitpid(external->pid, &s, 0);
external->pid = 0;
}
argp[2] = external->command;
log_debug("running %s", external->command);
pid = fork();
if (pid == -1) {
log_warn("fork error");
} else if (pid == 0) {
execv(_PATH_BSHELL, argp);
_exit(1);
/* NOTREACHED */
} else {
external->pid = pid;
}
if (!async) {
waitpid(external->pid, &s, 0);
external->pid = 0;
if (WIFEXITED(s))
external->prevstatus = WEXITSTATUS(s);
}
}
void
adjust_external_expressions(struct ifsd_state *state)
{
struct ifsd_external *external;
struct ifsd_expression_list expressions;
TAILQ_INIT(&expressions);
TAILQ_FOREACH(external, &state->external_tests, entries) {
struct ifsd_expression *expression;
if (external->prevstatus == -1)
continue;
TAILQ_FOREACH(expression, &external->expressions, entries) {
TAILQ_INSERT_TAIL(&expressions,
expression, eval);
expression->truth = !external->prevstatus;
}
adjust_expressions(&expressions, conf->maxdepth);
}
}
void
check_external_status(struct ifsd_state *state)
{
struct ifsd_external *external, *end = NULL;
int status, s, changed = 0;
/* Do this manually; change ordering so the oldest is first */
external = TAILQ_FIRST(&state->external_tests);
while (external != NULL && external != end) {
struct ifsd_external *newexternal;
newexternal = TAILQ_NEXT(external, entries);
if (external->pid <= 0)
goto loop;
if (wait4(external->pid, &s, WNOHANG, NULL) == 0)
goto loop;
external->pid = 0;
if (end == NULL)
end = external;
if (WIFEXITED(s))
status = WEXITSTATUS(s);
else {
log_warnx("%s exited abnormally", external->command);
goto loop;
}
if (external->prevstatus != status &&
(external->prevstatus != -1 || !opt_inhibit)) {
changed = 1;
external->prevstatus = status;
}
external->lastexec = time(NULL);
TAILQ_REMOVE(&state->external_tests, external, entries);
TAILQ_INSERT_TAIL(&state->external_tests, external, entries);
loop:
external = newexternal;
}
if (changed) {
adjust_external_expressions(state);
eval_state(state);
}
}
void
external_evtimer_setup(struct ifsd_state *state, int action)
{
struct ifsd_external *external;
int s;
if (state != NULL) {
switch (action) {
case IFSD_EVTIMER_ADD:
TAILQ_FOREACH(external,
&state->external_tests, entries) {
struct timeval tv;
/* run it once right away */
external_exec(external, 0);
/* schedule it for later */
timerclear(&tv);
tv.tv_sec = external->frequency;
evtimer_set(&external->ev, external_handler,
external);
evtimer_add(&external->ev, &tv);
}
break;
case IFSD_EVTIMER_DEL:
TAILQ_FOREACH(external,
&state->external_tests, entries) {
if (external->pid > 0) {
kill(external->pid, SIGKILL);
waitpid(external->pid, &s, 0);
external->pid = 0;
}
evtimer_del(&external->ev);
}
break;
}
}
}
#define LINK_STATE_IS_DOWN(_s) (!LINK_STATE_IS_UP((_s)))
int
scan_ifstate_single(const char *ifname, int s, struct ifsd_state *state)
{
struct ifsd_ifstate *ifstate;
struct ifsd_expression_list expressions;
int changed = 0;
TAILQ_INIT(&expressions);
TAILQ_FOREACH(ifstate, &state->interface_states, entries) {
if (strcmp(ifstate->ifname, ifname) == 0) {
if (ifstate->prevstate != s &&
(ifstate->prevstate != -1 || !opt_inhibit)) {
struct ifsd_expression *expression;
int truth;
truth =
(ifstate->ifstate == IFSD_LINKUNKNOWN &&
s == LINK_STATE_UNKNOWN) ||
(ifstate->ifstate == IFSD_LINKDOWN &&
LINK_STATE_IS_DOWN(s)) ||
(ifstate->ifstate == IFSD_LINKUP &&
LINK_STATE_IS_UP(s));
TAILQ_FOREACH(expression,
&ifstate->expressions, entries) {
expression->truth = truth;
TAILQ_INSERT_TAIL(&expressions,
expression, eval);
changed = 1;
}
ifstate->prevstate = s;
}
}
}
if (changed)
adjust_expressions(&expressions, conf->maxdepth);
return (changed);
}
void
scan_ifstate(const char *ifname, int s, int do_eval)
{
struct ifsd_state *state;
int cur_eval = 0;
if (scan_ifstate_single(ifname, s, &conf->initstate) && do_eval)
eval_state(&conf->initstate);
TAILQ_FOREACH(state, &conf->states, entries) {
if (scan_ifstate_single(ifname, s, state) &&
(do_eval && state == conf->curstate))
cur_eval = 1;
}
/* execute actions _after_ all expressions have been adjusted */
if (cur_eval)
eval_state(conf->curstate);
}
/*
* Do a bottom-up adjustment of the expression tree's truth value,
* level-by-level to ensure that each expression's subexpressions have been
* evaluated.
*/
void
adjust_expressions(struct ifsd_expression_list *expressions, int depth)
{
struct ifsd_expression_list nexpressions;
struct ifsd_expression *expression;
TAILQ_INIT(&nexpressions);
while ((expression = TAILQ_FIRST(expressions)) != NULL) {
TAILQ_REMOVE(expressions, expression, eval);
if (expression->depth == depth) {
struct ifsd_expression *te;
switch (expression->type) {
case IFSD_OPER_AND:
expression->truth = expression->left->truth &&
expression->right->truth;
break;
case IFSD_OPER_OR:
expression->truth = expression->left->truth ||
expression->right->truth;
break;
case IFSD_OPER_NOT:
expression->truth = !expression->right->truth;
break;
default:
break;
}
if (expression->parent != NULL) {
if (TAILQ_EMPTY(&nexpressions))
te = NULL;
TAILQ_FOREACH(te, &nexpressions, eval)
if (expression->parent == te)
break;
if (te == NULL)
TAILQ_INSERT_TAIL(&nexpressions,
expression->parent, eval);
}
} else
TAILQ_INSERT_TAIL(&nexpressions, expression, eval);
}
if (depth > 0)
adjust_expressions(&nexpressions, depth - 1);
}
void
eval_state(struct ifsd_state *state)
{
struct ifsd_external *external;
external = TAILQ_FIRST(&state->external_tests);
if (external == NULL || external->lastexec >= state->entered ||
external->lastexec == 0) {
do_action(state->body);
while (state_change()) {
do_action(conf->curstate->init);
do_action(conf->curstate->body);
}
}
}
int
state_change(void)
{
if (conf->nextstate != NULL && conf->curstate != conf->nextstate) {
log_info("changing state to %s", conf->nextstate->name);
if (conf->curstate != NULL) {
evtimer_del(&conf->curstate->ev);
external_evtimer_setup(conf->curstate,
IFSD_EVTIMER_DEL);
}
conf->curstate = conf->nextstate;
conf->nextstate = NULL;
conf->curstate->entered = time(NULL);
external_evtimer_setup(conf->curstate, IFSD_EVTIMER_ADD);
adjust_external_expressions(conf->curstate);
return (1);
}
return (0);
}
/*
* Run recursively through the tree of actions.
*/
void
do_action(struct ifsd_action *action)
{
struct ifsd_action *subaction;
switch (action->type) {
case IFSD_ACTION_COMMAND:
log_debug("running %s", action->act.command);
system(action->act.command);
break;
case IFSD_ACTION_CHANGESTATE:
conf->nextstate = action->act.nextstate;
break;
case IFSD_ACTION_CONDITION:
if ((action->act.c.expression != NULL &&
action->act.c.expression->truth) ||
action->act.c.expression == NULL) {
TAILQ_FOREACH(subaction, &action->act.c.actions,
entries)
do_action(subaction);
}
break;
default:
log_debug("%s: unknown action %d", __func__, action->type);
break;
}
}
/*
* Fetch the current link states.
*/
void
fetch_ifstate(int do_eval)
{
struct ifaddrs *ifap, *ifa;
if (getifaddrs(&ifap) != 0)
fatal("getifaddrs");
for (ifa = ifap; ifa; ifa = ifa->ifa_next) {
if (ifa->ifa_addr != NULL &&
ifa->ifa_addr->sa_family == AF_LINK) {
struct if_data *ifdata = ifa->ifa_data;
scan_ifstate(ifa->ifa_name, ifdata->ifi_link_state,
do_eval);
}
}
freeifaddrs(ifap);
}
void
check_ifdeparture(void)
{
struct ifsd_state *state;
struct ifsd_ifstate *ifstate;
TAILQ_FOREACH(state, &conf->states, entries) {
TAILQ_FOREACH(ifstate, &state->interface_states, entries) {
if (if_nametoindex(ifstate->ifname) == 0)
scan_ifstate(ifstate->ifname,
LINK_STATE_DOWN, 1);
}
}
}
void
clear_config(struct ifsd_config *oconf)
{
struct ifsd_state *state;
external_evtimer_setup(&conf->initstate, IFSD_EVTIMER_DEL);
if (conf != NULL && conf->curstate != NULL)
external_evtimer_setup(conf->curstate, IFSD_EVTIMER_DEL);
while ((state = TAILQ_FIRST(&oconf->states)) != NULL) {
TAILQ_REMOVE(&oconf->states, state, entries);
remove_action(state->init, state);
remove_action(state->body, state);
free(state->name);
free(state);
}
remove_action(oconf->initstate.init, &oconf->initstate);
remove_action(oconf->initstate.body, &oconf->initstate);
free(oconf);
}
void
remove_action(struct ifsd_action *action, struct ifsd_state *state)
{
struct ifsd_action *subaction;
if (action == NULL || state == NULL)
return;
switch (action->type) {
case IFSD_ACTION_COMMAND:
free(action->act.command);
break;
case IFSD_ACTION_CHANGESTATE:
break;
case IFSD_ACTION_CONDITION:
if (action->act.c.expression != NULL)
remove_expression(action->act.c.expression, state);
while ((subaction =
TAILQ_FIRST(&action->act.c.actions)) != NULL) {
TAILQ_REMOVE(&action->act.c.actions,
subaction, entries);
remove_action(subaction, state);
}
}
free(action);
}
void
remove_expression(struct ifsd_expression *expression,
struct ifsd_state *state)
{
switch (expression->type) {
case IFSD_OPER_IFSTATE:
TAILQ_REMOVE(&expression->u.ifstate->expressions, expression,
entries);
if (--expression->u.ifstate->refcount == 0) {
TAILQ_REMOVE(&state->interface_states,
expression->u.ifstate, entries);
free(expression->u.ifstate);
}
break;
case IFSD_OPER_EXTERNAL:
TAILQ_REMOVE(&expression->u.external->expressions, expression,
entries);
if (--expression->u.external->refcount == 0) {
TAILQ_REMOVE(&state->external_tests,
expression->u.external, entries);
free(expression->u.external->command);
event_del(&expression->u.external->ev);
free(expression->u.external);
}
break;
default:
if (expression->left != NULL)
remove_expression(expression->left, state);
if (expression->right != NULL)
remove_expression(expression->right, state);
break;
}
free(expression);
}