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

2980
cfg.c

File diff suppressed because it is too large Load Diff

22
cfg.rl
View File

@ -85,6 +85,9 @@ struct cfgparse {
action state_dir {
copy_cmdarg(state_dir, ccfg.buf, sizeof state_dir, "state-dir");
}
action script_file {
copy_cmdarg(script_file, ccfg.buf, sizeof script_file, "script-file");
}
action seccomp_enforce {
log_line("seccomp_enforce option is deprecated; please remove it");
log_line("In the meanwhile, it is ignored and seccomp is disabled.");
@ -184,6 +187,7 @@ struct cfgparse {
sockd_user = 'sockd-user' value @sockd_user;
chroot = 'chroot' value @chroot;
state_dir = 'state-dir' value @state_dir;
script_file = 'script-file' value @script_file;
seccomp_enforce = 'seccomp-enforce' boolval @seccomp_enforce;
relentless_defense = 'relentless-defense' boolval @relentless_defense;
arp_probe_wait = 'arp-probe-wait' value @arp_probe_wait;
@ -199,9 +203,9 @@ struct cfgparse {
main := blankline |
clientid | hostname | interface | now |
request | vendorid | user | ifch_user | sockd_user | chroot |
state_dir | seccomp_enforce | relentless_defense | arp_probe_wait |
arp_probe_num | arp_probe_min | arp_probe_max | gw_metric |
resolv_conf | dhcp_set_hostname | rfkill_idx | s6_notify
state_dir | script_file | seccomp_enforce | relentless_defense |
arp_probe_wait | arp_probe_num | arp_probe_min | arp_probe_max |
gw_metric | resolv_conf | dhcp_set_hostname | rfkill_idx | s6_notify
;
}%%
@ -287,6 +291,7 @@ static void parse_cfgfile(const char *fname)
sockd_user = ('-D'|'--sockd-user') argval @sockd_user;
chroot = ('-C'|'--chroot') argval @chroot;
state_dir = ('-s'|'--state-dir') argval @state_dir;
script_file = ('-X'|'--script-file') argval @script_file;
seccomp_enforce = ('-S'|'--seccomp-enforce') tbv @seccomp_enforce;
relentless_defense = ('-d'|'--relentless-defense') tbv @relentless_defense;
arp_probe_wait = ('-w'|'--arp-probe-wait') argval @arp_probe_wait;
@ -302,12 +307,11 @@ static void parse_cfgfile(const char *fname)
help = ('-?'|'--help') 0 @help;
main := (
cfgfile | clientid | hostname | interface |
now | request | vendorid | user | ifch_user | sockd_user |
chroot | state_dir | seccomp_enforce | relentless_defense |
arp_probe_wait | arp_probe_num | arp_probe_min | arp_probe_max |
gw_metric | resolv_conf | dhcp_set_hostname | rfkill_idx | s6_notify |
version | help
cfgfile | clientid | hostname | interface | now | request | vendorid |
user | ifch_user | sockd_user | chroot | state_dir | script_file |
seccomp_enforce | relentless_defense | arp_probe_wait | arp_probe_num |
arp_probe_min | arp_probe_max | gw_metric | resolv_conf |
dhcp_set_hostname | rfkill_idx | s6_notify | version | help
)*;
}%%

View File

@ -15,6 +15,7 @@
#include "nk/io.h"
#include "leasefile.h"
#include "ndhc.h"
#include "scriptd.h"
static int leasefilefd = -1;
@ -40,15 +41,8 @@ void open_leasefile(void)
client_config.interface, __func__, leasefile, strerror(errno));
}
void write_leasefile(struct in_addr ipnum)
static void do_write_leasefile(struct in_addr ipnum)
{
if (client_config.enable_s6_notify) {
static char buf[] = "\n";
safe_write(client_config.s6_notify_fd, buf, 1);
close(client_config.s6_notify_fd);
client_config.enable_s6_notify = false;
}
char ip[INET_ADDRSTRLEN];
char out[INET_ADDRSTRLEN*2];
if (leasefilefd < 0) {
@ -82,3 +76,16 @@ void write_leasefile(struct in_addr ipnum)
fsync(leasefilefd);
}
void write_leasefile(struct in_addr ipnum)
{
do_write_leasefile(ipnum);
request_scriptd_run();
if (client_config.enable_s6_notify) {
static char buf[] = "\n";
safe_write(client_config.s6_notify_fd, buf, 1);
close(client_config.s6_notify_fd);
client_config.enable_s6_notify = false;
}
}

12
ndhc.8
View File

@ -41,8 +41,7 @@ Specifies the directory where the DHCP state associated with the given
interface will be stored. Such state will include the leased IP, the
IAID, and the DUID. The file representing the leased IP can be quite
useful for reacting to changes in IP address -- one can listen for changes
to it using fanotify() or inotify() on Linux. The 'fcactus' program
written by the ndhc author is designed to perform this task.
to it using fanotify() or inotify() on Linux.
.TP
.BI \-i\ INTERFACE ,\ \-\-interface= INTERFACE
Act as a DHCP client for the specified interface. A single ndhc daemon can
@ -131,6 +130,15 @@ and closed when the first DHCP lease is bound. This option should be used when
ndhc is run under a s6 supervisor that implements service startup
notifications.
.TP
.BI \-X\ SCRIPTFILE ,\ \-\-script\-file= SCRIPTFILE
If set, ndhc will spawn a subprocess that has the exclusive job of executing
the specified executable file immediately after a new lease is acquired. This
script file will run as root and will not be chrooted. It will be provided a
sanitized environment that has no inputs from the dhcp state. If this
parameter is not provided, then the ndhc-scriptd subprocess will not exist.
This facility is intended to be used for updating firewall/nat rules or similar
tasks.
.TP
.BI \-v ,\ \-\-version
Display the ndhc version number.
.SH SIGNALS

38
ndhc.c
View File

@ -43,6 +43,7 @@
#include "duiaid.h"
#include "sockd.h"
#include "rfkill.h"
#include "scriptd.h"
struct client_state_t cs = {
.program_init = true,
@ -400,12 +401,15 @@ static void do_ndhc_work(void)
char state_dir[PATH_MAX] = "/etc/ndhc";
char chroot_dir[PATH_MAX] = "";
char resolv_conf_d[PATH_MAX] = "";
char script_file[PATH_MAX] = "";
uid_t ndhc_uid = 0;
gid_t ndhc_gid = 0;
int ifchSock[2];
int sockdSock[2];
int ifchStream[2];
int sockdSock[2];
int sockdStream[2];
int scriptdSock[2];
int scriptdStream[2];
static void create_ifch_ipc_sockets(void) {
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, ifchSock) < 0)
@ -418,7 +422,14 @@ static void create_sockd_ipc_sockets(void) {
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sockdSock) < 0)
suicide("FATAL - can't create ndhc/sockd socket: %s", strerror(errno));
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockdStream) < 0)
suicide("FATAL - can't create ndhc/ifch socket: %s", strerror(errno));
suicide("FATAL - can't create ndhc/sockd socket: %s", strerror(errno));
}
static void create_scriptd_ipc_sockets(void) {
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, scriptdSock) < 0)
suicide("FATAL - can't create ndhc/scriptd socket: %s", strerror(errno));
if (socketpair(AF_UNIX, SOCK_STREAM, 0, scriptdStream) < 0)
suicide("FATAL - can't create ndhc/scriptd socket: %s", strerror(errno));
}
static void spawn_ifch(void)
@ -455,6 +466,28 @@ static void spawn_sockd(void)
suicide("failed to fork ndhc-sockd: %s", strerror(errno));
}
static void spawn_scriptd(void)
{
valid_script_file = access(script_file, R_OK | X_OK) == 0;
if (!valid_script_file) return;
log_line("Found script file: '%s'", script_file);
create_scriptd_ipc_sockets();
pid_t scriptd_pid = fork();
if (scriptd_pid == 0) {
close(scriptdSock[0]);
close(scriptdStream[0]);
// Don't share the RNG state with the master process.
nk_random_init(&cs.rnd_state);
scriptd_main();
} else if (scriptd_pid > 0) {
close(scriptdSock[1]);
close(scriptdStream[1]);
} else
suicide("failed to fork ndhc-scriptd: %s", strerror(errno));
}
static void ndhc_main(void) {
prctl(PR_SET_NAME, "ndhc: master");
log_line("ndhc client " NDHC_VERSION " started on interface [%s].",
@ -548,6 +581,7 @@ int main(int argc, char *argv[])
spawn_ifch();
spawn_sockd();
spawn_scriptd();
ndhc_main();
exit(EXIT_SUCCESS);
}

3
ndhc.h
View File

@ -69,9 +69,12 @@ extern int ifchSock[2];
extern int ifchStream[2];
extern int sockdSock[2];
extern int sockdStream[2];
extern int scriptdSock[2];
extern int scriptdStream[2];
extern char state_dir[PATH_MAX];
extern char chroot_dir[PATH_MAX];
extern char resolv_conf_d[PATH_MAX];
extern char script_file[PATH_MAX];
extern uid_t ndhc_uid;
extern gid_t ndhc_gid;

191
nk/exec.c Normal file
View File

@ -0,0 +1,191 @@
// Copyright 2003-2018 Nicholas J. Kain <njkain at gmail dot com>
// SPDX-License-Identifier: MIT
#include <sys/types.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <pwd.h>
#include "nk/exec.h"
#define DEFAULT_ROOT_PATH "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
#define DEFAULT_PATH "/bin:/usr/bin:/usr/local/bin"
#define MAX_ARGS 256
#define MAX_ARGBUF 4096
#define NK_GEN_ENV(GEN_STR, ...) do { \
if (env_offset >= envlen) return -3; \
ssize_t snlen = snprintf(envbuf, envbuflen, GEN_STR "0", __VA_ARGS__); \
if (snlen < 0 || (size_t)snlen >= envbuflen) return -2; \
if (snlen > 0) envbuf[snlen-1] = 0; \
env[env_offset++] = envbuf; envbuf += snlen; envbuflen -= (size_t)snlen; \
} while (0)
/*
* uid: userid of the user account that the environment will constructed for
* chroot_path: path where the environment will be chrooted or NULL if no chroot
* path_var: value of the PATH variable in the environment or defaults if NULL
* env: array of character pointers that will be filled in with the new environment
* envlen: number of character pointers available in env; a terminal '0' ptr must be available
* envbuf: character buffer that will be used for storing state associated with env
* envbuflen: number of available characters in envbuf for use
*
* returns:
* 0 on success
* -1 if an account for uid does not exist
* -2 if there is not enough space in envbuf for the generated environment
* -3 if there is not enough space in env for the generated environment
* -4 if chdir to homedir or rootdir failed
* -5 if oom or i/o failed
*/
int nk_generate_env(uid_t uid, const char *chroot_path, const char *path_var,
char *env[], size_t envlen, char *envbuf, size_t envbuflen)
{
char pw_strs[4096];
struct passwd pw_s;
struct passwd *pw;
char *pw_buf = NULL;
int ret = 0, pwr;
getpwagain0:
pwr = getpwuid_r(uid, &pw_s, pw_strs, sizeof pw_strs, &pw);
if (pw == NULL) {
if (pwr == 0) { ret = -1; goto out; }
if (pwr == EINTR) goto getpwagain0;
if (pwr == ERANGE) {
size_t pwlen = (sizeof pw_strs >> 1) * 3;
for (;;) {
if (pw_buf) free(pw_buf);
pw_buf = malloc(pwlen);
if (!pw_buf) { ret = -5; goto out; }
getpwagain:
pwr = getpwuid_r(uid, &pw_s, pw_buf, pwlen, &pw);
if (pw == NULL) {
if (pwr == 0) { ret = -1; goto out; }
if (pwr == EINTR) goto getpwagain;
if (pwr == ERANGE) {
size_t oldpwlen = pwlen;
pwlen = (pwlen >> 1) * 3;
if (pwlen > oldpwlen) continue;
else { // overflowed
ret = -5; goto out;
}
}
ret = -5; goto out;
}
break; // the pwr != 0 check below applies here
}
}
ret = -5; goto out;
}
if (pwr != 0) { ret = -5; goto out; }
size_t env_offset = 0;
if (envlen-- < 1) { // So we don't have to account for the terminal NULL
ret = -3;
goto out;
}
NK_GEN_ENV("UID=%i", uid);
NK_GEN_ENV("USER=%s", pw->pw_name);
NK_GEN_ENV("USERNAME=%s", pw->pw_name);
NK_GEN_ENV("LOGNAME=%s", pw->pw_name);
NK_GEN_ENV("HOME=%s", pw->pw_dir);
NK_GEN_ENV("SHELL=%s", pw->pw_shell);
NK_GEN_ENV("PATH=%s", path_var ? path_var : (uid > 0 ? DEFAULT_PATH : DEFAULT_ROOT_PATH));
NK_GEN_ENV("PWD=%s", !chroot_path ? pw->pw_dir : "/");
if (chroot_path && chroot(chroot_path)) { ret = -4; goto out; }
if (chdir(chroot_path ? chroot_path : "/")) { ret = -4; goto out; }
env[env_offset] = 0;
out:
free(pw_buf);
return ret;
}
#define NK_GEN_ARG(GEN_STR, ...) do { \
ssize_t snlen = snprintf(argbuf, argbuflen, GEN_STR "0", __VA_ARGS__); \
if (snlen < 0 || (size_t)snlen >= argbuflen) { \
static const char errstr[] = "nk_execute: constructing argument list failed\n"; \
write(STDERR_FILENO, errstr, sizeof errstr); \
_Exit(EXIT_FAILURE); \
} \
if (snlen > 0) argbuf[snlen-1] = 0; \
argv[curv] = argbuf; argv[++curv] = (char *)0; \
argbuf += snlen; argbuflen -= (size_t)snlen; \
} while (0)
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
#endif
void __attribute__((noreturn))
nk_execute(const char *command, const char *args, char * const envp[])
{
char *argv[MAX_ARGS];
char argbuf_s[MAX_ARGBUF];
char *argbuf = argbuf_s;
size_t curv = 0;
size_t argbuflen = sizeof argbuf_s;
if (!command)
_Exit(EXIT_SUCCESS);
// strip the path from the command name and set argv[0]
const char *p = strrchr(command, '/');
NK_GEN_ARG("%s", p ? p + 1 : command);
if (args) {
p = args;
const char *q = args;
bool squote = false, dquote = false, atend = false;
for (;; ++p) {
switch (*p) {
default: continue;
case '\0':
atend = true;
goto endarg;
case ' ':
if (!squote && !dquote)
goto endarg;
continue;
case '\'':
if (!dquote)
squote = !squote;
continue;
case '"':
if (!squote)
dquote = !dquote;
continue;
}
endarg:
{
if (p == q) break;
// Push an argument.
if (q > p) {
static const char errstr[] = "nk_execute: argument length too long\n";
write(STDERR_FILENO, errstr, sizeof errstr);
_Exit(EXIT_FAILURE);
}
const size_t len = (size_t)(p - q);
NK_GEN_ARG("%.*s", (int)len, q);
q = p + 1;
if (atend || curv >= (MAX_ARGS - 1))
break;
}
}
}
execve(command, argv, envp);
{
static const char errstr[] = "nk_execute: execve failed\n";
write(STDERR_FILENO, errstr, sizeof errstr);
_Exit(EXIT_FAILURE);
}
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

13
nk/exec.h Normal file
View File

@ -0,0 +1,13 @@
// Copyright 2003-2016 Nicholas J. Kain <njkain at gmail dot com>
// SPDX-License-Identifier: MIT
#ifndef NCM_EXEC_H_
#define NCM_EXEC_H_
int nk_generate_env(uid_t uid, const char *chroot_path, const char *path_var,
char *env[], size_t envlen, char *envbuf, size_t envbuflen);
void __attribute__((noreturn))
nk_execute(const char *command, const char *args, char * const envp[]) ;
#endif

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();
}

14
scriptd.h Normal file
View File

@ -0,0 +1,14 @@
// Copyright 2022 Nicholas J. Kain <njkain at gmail dot com>
// SPDX-License-Identifier: MIT
#ifndef NDHC_SCRIPTD_H_
#define NDHC_SCRIPTD_H_
#include <stdbool.h>
extern bool valid_script_file;
void request_scriptd_run(void);
void scriptd_main(void);
#endif