Support running an executable file when a new lease is acquired.

If no 'script-file = SCRIPTFILE' is specified in the configuration
file and if no '-X SCRIPTFILE' or '--script-file SCRIPTFILE'
command argument is provided, then this functionality is entirely
inactive and no associated subprocess is spawned.

Otherwise, ndhc will spawn a subprocess that runs as root that has the
sole job of forking off a subprocess that exec's the specified script in
a sanitized and fixed-state environment whenever a new DHCPv4 lease is
acquired.

Note that this script is provided no information about ndhc or the
DHCP state in the environment or in any argument fields; it is the
responsibility of this script to gather whatever information it needs
from either the filesystem or syscalls.  This design is intended to
avoid the historical problems that are associated with dhcp clients
invoking scripts.

The path of the scriptfile cannot be changed after ndhc is initially
run; ndhc forks off the privsep script subprocess that executes scripts
after it has read the configuration file and command arguments, but
before it begins processing network data; thus, it is impossible for the
network-handling process to modify or influence the script assuming
proper OS memory protection.

The privsep channel communicates that the script should be run by simply
writing a newline; anything else will result in ndhc terminating itself.

Before the recommended way to update system state after a change in
lease information was to run the fcactus program and watch the
associated leasefile for the interface for modification; now no external
program is needed for this job.
This commit is contained in:
Nicholas J. Kain
2022-02-24 00:52:26 -05:00
parent 2fb16567f1
commit a9874d4959
10 changed files with 2246 additions and 1262 deletions

172
scriptd.c Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2022 Nicholas J. Kain <njkain at gmail dot com>
// SPDX-License-Identifier: MIT
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <poll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include "nk/log.h"
#include "nk/io.h"
#include "nk/exec.h"
#include "scriptd.h"
#include "ndhc.h"
#include "sys.h"
#define MAX_ENVBUF 2048
#define MAX_CENV 50
bool valid_script_file = false;
// Runs the 'script_file'-specified script. Called from ndhc process.
void request_scriptd_run(void)
{
if (!valid_script_file) return;
static char buf[] = "\n";
ssize_t r = safe_write(scriptdSock[0], buf, 1);
if (r < 0 || (size_t)r != 1)
suicide("%s: (%s) write failed: %zd", client_config.interface,
__func__, r);
}
static void run_script(void)
{
char *env[MAX_CENV];
char envbuf[MAX_ENVBUF];
switch ((int)fork()) {
case 0: {
int r = nk_generate_env(0, NULL, NULL, env, MAX_CENV, envbuf, sizeof envbuf);
if (r < 0) {
static const char errstr[] = "exec: failed to generate environment - ";
safe_write(STDERR_FILENO, errstr, sizeof errstr);
static const char errstr0[] = "(?) unknown error";
static const char errstr1[] = "(-1) account for uid does not exist";
static const char errstr2[] = "(-2) not enough space in envbuf";
static const char errstr3[] = "(-3) not enough space in env";
static const char errstr4[] = "(-4) chdir to homedir or rootdir failed";
switch (r) {
default: safe_write(STDERR_FILENO, errstr0, sizeof errstr0); break;
case -1: safe_write(STDERR_FILENO, errstr1, sizeof errstr1); break;
case -2: safe_write(STDERR_FILENO, errstr2, sizeof errstr2); break;
case -3: safe_write(STDERR_FILENO, errstr3, sizeof errstr3); break;
case -4: safe_write(STDERR_FILENO, errstr4, sizeof errstr4); break;
}
safe_write(STDERR_FILENO, "\n", 1);
exit(EXIT_FAILURE);
}
nk_execute(script_file, NULL, env);
}
case -1: {
static const char errstr[] = "exec: fork failed\n";
safe_write(STDERR_FILENO, errstr, sizeof errstr);
exit(EXIT_FAILURE);
}
default: break;
}
}
static void process_client_socket(void)
{
static char buf[32];
static size_t buflen;
if (buflen == sizeof buf)
suicide("%s: (%s) receive buffer exhausted", client_config.interface,
__func__);
int r = safe_recv(scriptdSock[1], buf + buflen, sizeof buf - buflen,
MSG_DONTWAIT);
if (r == 0) {
// Remote end hung up.
exit(EXIT_SUCCESS);
} else if (r < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
suicide("%s: (%s) error reading from ndhc -> scriptd socket: %s",
client_config.interface, __func__, strerror(errno));
}
buflen += (size_t)r;
if (buflen > 1 || buf[0] != '\n') exit(EXIT_SUCCESS);
buflen = 0;
run_script();
}
static void do_scriptd_work(void)
{
struct pollfd pfds[2] = {0};
pfds[0].fd = scriptdSock[1];
pfds[0].events = POLLIN|POLLHUP|POLLERR|POLLRDHUP;
pfds[1].fd = scriptdStream[1];
pfds[1].events = POLLHUP|POLLERR|POLLRDHUP;
for (;;) {
if (poll(pfds, 2, -1) < 0) {
if (errno != EINTR) suicide("poll failed");
}
if (pfds[0].revents & POLLIN) {
process_client_socket();
}
if (pfds[0].revents & (POLLHUP|POLLERR|POLLRDHUP)) {
suicide("scriptdSock closed unexpectedly");
}
if (pfds[1].revents & (POLLHUP|POLLERR|POLLRDHUP)) {
exit(EXIT_SUCCESS);
}
}
}
static void signal_handler(int signo)
{
switch (signo) {
case SIGCHLD:
while (waitpid(-1, NULL, WNOHANG) > 0);
break;
case SIGINT:
case SIGTERM: _exit(EXIT_FAILURE); break;
default: break;
}
}
static void setup_signals_scriptd(void)
{
static const int ss[] = {
SIGCHLD, SIGINT, SIGTERM, SIGKILL
};
sigset_t mask;
if (sigprocmask(0, 0, &mask) < 0)
suicide("sigprocmask failed");
for (int i = 0; ss[i] != SIGKILL; ++i)
if (sigdelset(&mask, ss[i]))
suicide("sigdelset failed");
if (sigaddset(&mask, SIGPIPE))
suicide("sigaddset failed");
if (sigprocmask(SIG_SETMASK, &mask, (sigset_t *)0) < 0)
suicide("sigprocmask failed");
struct sigaction sa = {
.sa_handler = signal_handler,
.sa_flags = SA_RESTART|SA_NOCLDWAIT,
};
if (sigemptyset(&sa.sa_mask))
suicide("sigemptyset failed");
for (int i = 0; ss[i] != SIGKILL; ++i)
if (sigaction(ss[i], &sa, NULL))
suicide("sigaction failed");
}
void scriptd_main(void)
{
assert(valid_script_file);
prctl(PR_SET_NAME, "ndhc: scriptd");
umask(077);
setup_signals_scriptd();
do_scriptd_work();
}