diff options
Diffstat (limited to 'src/script.c')
-rw-r--r-- | src/script.c | 788 |
1 files changed, 788 insertions, 0 deletions
diff --git a/src/script.c b/src/script.c new file mode 100644 index 000000000000..6173b4020f38 --- /dev/null +++ b/src/script.c @@ -0,0 +1,788 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * dhcpcd - DHCP client daemon + * Copyright (c) 2006-2021 Roy Marples <roy@marples.name> + * All rights reserved + + * 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 AUTHOR 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 AUTHOR 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/stat.h> +#include <sys/uio.h> +#include <sys/wait.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <assert.h> +#include <ctype.h> +#include <errno.h> +#include <pwd.h> +#include <signal.h> +#include <spawn.h> +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "config.h" +#include "common.h" +#include "dhcp.h" +#include "dhcp6.h" +#include "eloop.h" +#include "if.h" +#include "if-options.h" +#include "ipv4ll.h" +#include "ipv6nd.h" +#include "logerr.h" +#include "privsep.h" +#include "script.h" + +#define DEFAULT_PATH "/usr/bin:/usr/sbin:/bin:/sbin" + +static const char * const if_params[] = { + "interface", + "protocol", + "reason", + "pid", + "ifcarrier", + "ifmetric", + "ifwireless", + "ifflags", + "ssid", + "profile", + "interface_order", + NULL +}; + +static const char * true_str = "true"; +static const char * false_str = "false"; + +void +if_printoptions(void) +{ + const char * const *p; + + for (p = if_params; *p; p++) + printf(" - %s\n", *p); +} + +pid_t +script_exec(char *const *argv, char *const *env) +{ + pid_t pid = 0; + posix_spawnattr_t attr; + int r; +#ifdef USE_SIGNALS + size_t i; + short flags; + sigset_t defsigs; +#else + UNUSED(ctx); +#endif + + /* posix_spawn is a safe way of executing another image + * and changing signals back to how they should be. */ + if (posix_spawnattr_init(&attr) == -1) + return -1; +#ifdef USE_SIGNALS + flags = POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF; + posix_spawnattr_setflags(&attr, flags); + sigemptyset(&defsigs); + posix_spawnattr_setsigmask(&attr, &defsigs); + for (i = 0; i < dhcpcd_signals_len; i++) + sigaddset(&defsigs, dhcpcd_signals[i]); + for (i = 0; i < dhcpcd_signals_ignore_len; i++) + sigaddset(&defsigs, dhcpcd_signals_ignore[i]); + posix_spawnattr_setsigdefault(&attr, &defsigs); +#endif + errno = 0; + r = posix_spawn(&pid, argv[0], NULL, &attr, argv, env); + posix_spawnattr_destroy(&attr); + if (r) { + errno = r; + return -1; + } + return pid; +} + +#ifdef INET +static int +append_config(FILE *fp, const char *prefix, const char *const *config) +{ + size_t i; + + if (config == NULL) + return 0; + + /* Do we need to replace existing config rather than append? */ + for (i = 0; config[i] != NULL; i++) { + if (efprintf(fp, "%s_%s", prefix, config[i]) == -1) + return -1; + } + return 1; +} + +#endif + +#define PROTO_LINK 0 +#define PROTO_DHCP 1 +#define PROTO_IPV4LL 2 +#define PROTO_RA 3 +#define PROTO_DHCP6 4 +#define PROTO_STATIC6 5 +static const char *protocols[] = { + "link", + "dhcp", + "ipv4ll", + "ra", + "dhcp6", + "static6" +}; + +int +efprintf(FILE *fp, const char *fmt, ...) +{ + va_list args; + int r; + + va_start(args, fmt); + r = vfprintf(fp, fmt, args); + va_end(args); + if (r == -1) + return -1; + /* Write a trailing NULL so we can easily create env strings. */ + if (fputc('\0', fp) == EOF) + return -1; + return r; +} + +char ** +script_buftoenv(struct dhcpcd_ctx *ctx, char *buf, size_t len) +{ + char **env, **envp, *bufp, *endp; + size_t nenv; + + /* Count the terminated env strings. + * Assert that the terminations are correct. */ + nenv = 0; + endp = buf + len; + for (bufp = buf; bufp < endp; bufp++) { + if (*bufp == '\0') { +#ifndef NDEBUG + if (bufp + 1 < endp) + assert(*(bufp + 1) != '\0'); +#endif + nenv++; + } + } + assert(*(bufp - 1) == '\0'); + if (nenv == 0) + return NULL; + + if (ctx->script_envlen < nenv) { + env = reallocarray(ctx->script_env, nenv + 1, sizeof(*env)); + if (env == NULL) + return NULL; + ctx->script_env = env; + ctx->script_envlen = nenv; + } + + bufp = buf; + envp = ctx->script_env; + *envp++ = bufp++; + endp--; /* Avoid setting the last \0 to an invalid pointer */ + for (; bufp < endp; bufp++) { + if (*bufp == '\0') + *envp++ = bufp + 1; + } + *envp = NULL; + + return ctx->script_env; +} + +static long +make_env(struct dhcpcd_ctx *ctx, const struct interface *ifp, + const char *reason) +{ + FILE *fp; + long buf_pos, i; + char *path; + int protocol = PROTO_LINK; + const struct if_options *ifo; + const struct interface *ifp2; + int af; + bool is_stdin = ifp->name[0] == '\0'; + const char *if_up, *if_down; + rb_tree_t ifaces; + struct rt *rt; +#ifdef INET + const struct dhcp_state *state; +#ifdef IPV4LL + const struct ipv4ll_state *istate; +#endif +#endif +#ifdef DHCP6 + const struct dhcp6_state *d6_state; +#endif + +#ifdef HAVE_OPEN_MEMSTREAM + if (ctx->script_fp == NULL) { + fp = open_memstream(&ctx->script_buf, &ctx->script_buflen); + if (fp == NULL) + goto eexit; + ctx->script_fp = fp; + } else { + fp = ctx->script_fp; + rewind(fp); + } +#else + char tmpfile[] = "/tmp/dhcpcd-script-env-XXXXXX"; + int tmpfd; + + fp = NULL; + tmpfd = mkstemp(tmpfile); + if (tmpfd == -1) { + logerr("%s: mkstemp", __func__); + return -1; + } + unlink(tmpfile); + fp = fdopen(tmpfd, "w+"); + if (fp == NULL) { + close(tmpfd); + goto eexit; + } +#endif + + if (!(ifp->ctx->options & DHCPCD_DUMPLEASE)) { + /* Needed for scripts */ + path = getenv("PATH"); + if (efprintf(fp, "PATH=%s", + path == NULL ? DEFAULT_PATH : path) == -1) + goto eexit; + if (efprintf(fp, "pid=%d", getpid()) == -1) + goto eexit; + } + + if (!is_stdin) { + if (efprintf(fp, "reason=%s", reason) == -1) + goto eexit; + } + + ifo = ifp->options; +#ifdef INET + state = D_STATE(ifp); +#ifdef IPV4LL + istate = IPV4LL_CSTATE(ifp); +#endif +#endif +#ifdef DHCP6 + d6_state = D6_CSTATE(ifp); +#endif + if (strcmp(reason, "TEST") == 0) { + if (1 == 2) { + /* This space left intentionally blank + * as all the below statements are optional. */ + } +#ifdef INET6 +#ifdef DHCP6 + else if (d6_state && d6_state->new) + protocol = PROTO_DHCP6; +#endif + else if (ipv6nd_hasra(ifp)) + protocol = PROTO_RA; +#endif +#ifdef INET +#ifdef IPV4LL + else if (istate && istate->addr != NULL) + protocol = PROTO_IPV4LL; +#endif + else + protocol = PROTO_DHCP; +#endif + } +#ifdef INET6 + else if (strcmp(reason, "STATIC6") == 0) + protocol = PROTO_STATIC6; +#ifdef DHCP6 + else if (reason[strlen(reason) - 1] == '6') + protocol = PROTO_DHCP6; +#endif + else if (strcmp(reason, "ROUTERADVERT") == 0) + protocol = PROTO_RA; +#endif + else if (strcmp(reason, "PREINIT") == 0 || + strcmp(reason, "CARRIER") == 0 || + strcmp(reason, "NOCARRIER") == 0 || + strcmp(reason, "NOCARRIER_ROAMING") == 0 || + strcmp(reason, "UNKNOWN") == 0 || + strcmp(reason, "DEPARTED") == 0 || + strcmp(reason, "STOPPED") == 0) + protocol = PROTO_LINK; +#ifdef INET +#ifdef IPV4LL + else if (strcmp(reason, "IPV4LL") == 0) + protocol = PROTO_IPV4LL; +#endif + else + protocol = PROTO_DHCP; +#endif + + if (!is_stdin) { + if (efprintf(fp, "interface=%s", ifp->name) == -1) + goto eexit; + if (protocols[protocol] != NULL) { + if (efprintf(fp, "protocol=%s", + protocols[protocol]) == -1) + goto eexit; + } + } + if (ifp->ctx->options & DHCPCD_DUMPLEASE && protocol != PROTO_LINK) + goto dumplease; + if (efprintf(fp, "if_configured=%s", + ifo->options & DHCPCD_CONFIGURE ? "true" : "false") == -1) + goto eexit; + if (efprintf(fp, "ifcarrier=%s", + ifp->carrier == LINK_UNKNOWN ? "unknown" : + ifp->carrier == LINK_UP ? "up" : "down") == -1) + goto eexit; + if (efprintf(fp, "ifmetric=%d", ifp->metric) == -1) + goto eexit; + if (efprintf(fp, "ifwireless=%d", ifp->wireless) == -1) + goto eexit; + if (efprintf(fp, "ifflags=%u", ifp->flags) == -1) + goto eexit; + if (efprintf(fp, "ifmtu=%d", if_getmtu(ifp)) == -1) + goto eexit; + if (ifp->wireless) { + char pssid[IF_SSIDLEN * 4]; + + if (print_string(pssid, sizeof(pssid), OT_ESCSTRING, + ifp->ssid, ifp->ssid_len) != -1) + { + if (efprintf(fp, "ifssid=%s", pssid) == -1) + goto eexit; + } + } + if (*ifp->profile != '\0') { + if (efprintf(fp, "profile=%s", ifp->profile) == -1) + goto eexit; + } + if (ifp->ctx->options & DHCPCD_DUMPLEASE) + goto dumplease; + + ifp->ctx->rt_order = 0; + rb_tree_init(&ifaces, &rt_compare_proto_ops); + TAILQ_FOREACH(ifp2, ifp->ctx->ifaces, next) { + if (!ifp2->active) + continue; + rt = rt_new(UNCONST(ifp2)); + if (rt == NULL) + goto eexit; + if (rt_proto_add(&ifaces, rt) != rt) + goto eexit; + } + if (fprintf(fp, "interface_order=") == -1) + goto eexit; + RB_TREE_FOREACH(rt, &ifaces) { + if (rt != RB_TREE_MIN(&ifaces) && + fprintf(fp, "%s", " ") == -1) + goto eexit; + if (fprintf(fp, "%s", rt->rt_ifp->name) == -1) + goto eexit; + } + rt_headclear(&ifaces, AF_UNSPEC); + if (fputc('\0', fp) == EOF) + goto eexit; + + if (strcmp(reason, "STOPPED") == 0) { + if_up = false_str; + if_down = ifo->options & DHCPCD_RELEASE ? true_str : false_str; + } else if (strcmp(reason, "TEST") == 0 || + strcmp(reason, "PREINIT") == 0 || + strcmp(reason, "CARRIER") == 0 || + strcmp(reason, "UNKNOWN") == 0) + { + if_up = false_str; + if_down = false_str; + } else if (strcmp(reason, "NOCARRIER") == 0) { + if_up = false_str; + if_down = true_str; + } else if (strcmp(reason, "NOCARRIER_ROAMING") == 0) { + if_up = true_str; + if_down = false_str; + } else if (1 == 2 /* appease ifdefs */ +#ifdef INET + || (protocol == PROTO_DHCP && state && state->new) +#ifdef IPV4LL + || (protocol == PROTO_IPV4LL && IPV4LL_STATE_RUNNING(ifp)) +#endif +#endif +#ifdef INET6 + || (protocol == PROTO_STATIC6 && IPV6_STATE_RUNNING(ifp)) +#ifdef DHCP6 + || (protocol == PROTO_DHCP6 && d6_state && d6_state->new) +#endif + || (protocol == PROTO_RA && ipv6nd_hasra(ifp)) +#endif + ) + { + if_up = true_str; + if_down = false_str; + } else { + if_up = false_str; + if_down = true_str; + } + if (efprintf(fp, "if_up=%s", if_up) == -1) + goto eexit; + if (efprintf(fp, "if_down=%s", if_down) == -1) + goto eexit; + + if ((af = dhcpcd_ifafwaiting(ifp)) != AF_MAX) { + if (efprintf(fp, "if_afwaiting=%d", af) == -1) + goto eexit; + } + if ((af = dhcpcd_afwaiting(ifp->ctx)) != AF_MAX) { + TAILQ_FOREACH(ifp2, ifp->ctx->ifaces, next) { + if ((af = dhcpcd_ifafwaiting(ifp2)) != AF_MAX) + break; + } + } + if (af != AF_MAX) { + if (efprintf(fp, "af_waiting=%d", af) == -1) + goto eexit; + } + if (ifo->options & DHCPCD_DEBUG) { + if (efprintf(fp, "syslog_debug=true") == -1) + goto eexit; + } +#ifdef INET + if (protocol == PROTO_DHCP && state && state->old) { + if (dhcp_env(fp, "old", ifp, + state->old, state->old_len) == -1) + goto eexit; + if (append_config(fp, "old", + (const char *const *)ifo->config) == -1) + goto eexit; + } +#endif +#ifdef DHCP6 + if (protocol == PROTO_DHCP6 && d6_state && d6_state->old) { + if (dhcp6_env(fp, "old", ifp, + d6_state->old, d6_state->old_len) == -1) + goto eexit; + } +#endif + +dumplease: +#ifdef INET +#ifdef IPV4LL + if (protocol == PROTO_IPV4LL && istate) { + if (ipv4ll_env(fp, istate->down ? "old" : "new", ifp) == -1) + goto eexit; + } +#endif + if (protocol == PROTO_DHCP && state && state->new) { + if (dhcp_env(fp, "new", ifp, + state->new, state->new_len) == -1) + goto eexit; + if (append_config(fp, "new", + (const char *const *)ifo->config) == -1) + goto eexit; + } +#endif +#ifdef INET6 + if (protocol == PROTO_STATIC6) { + if (ipv6_env(fp, "new", ifp) == -1) + goto eexit; + } +#ifdef DHCP6 + if (protocol == PROTO_DHCP6 && D6_STATE_RUNNING(ifp)) { + if (dhcp6_env(fp, "new", ifp, + d6_state->new, d6_state->new_len) == -1) + goto eexit; + } +#endif + if (protocol == PROTO_RA) { + if (ipv6nd_env(fp, ifp) == -1) + goto eexit; + } +#endif + + /* Add our base environment */ + if (ifo->environ) { + for (i = 0; ifo->environ[i] != NULL; i++) + if (efprintf(fp, "%s", ifo->environ[i]) == -1) + goto eexit; + } + + /* Convert buffer to argv */ + fflush(fp); + + buf_pos = ftell(fp); + if (buf_pos == -1) { + logerr(__func__); + goto eexit; + } + +#ifndef HAVE_OPEN_MEMSTREAM + size_t buf_len = (size_t)buf_pos; + if (ctx->script_buflen < buf_len) { + char *buf = realloc(ctx->script_buf, buf_len); + if (buf == NULL) + goto eexit; + ctx->script_buf = buf; + ctx->script_buflen = buf_len; + } + rewind(fp); + if (fread(ctx->script_buf, sizeof(char), buf_len, fp) != buf_len) + goto eexit; + fclose(fp); + fp = NULL; +#endif + + if (is_stdin) + return buf_pos; + + if (script_buftoenv(ctx, ctx->script_buf, (size_t)buf_pos) == NULL) + goto eexit; + + return buf_pos; + +eexit: + logerr(__func__); +#ifndef HAVE_OPEN_MEMSTREAM + if (fp != NULL) + fclose(fp); +#endif + return -1; +} + +static int +send_interface1(struct fd_list *fd, const struct interface *ifp, + const char *reason) +{ + struct dhcpcd_ctx *ctx = ifp->ctx; + long len; + + len = make_env(ifp->ctx, ifp, reason); + if (len == -1) + return -1; + return control_queue(fd, ctx->script_buf, (size_t)len); +} + +int +send_interface(struct fd_list *fd, const struct interface *ifp, int af) +{ + int retval = 0; +#ifdef INET + const struct dhcp_state *d; +#endif +#ifdef DHCP6 + const struct dhcp6_state *d6; +#endif + +#ifndef AF_LINK +#define AF_LINK AF_PACKET +#endif + + if (af == AF_UNSPEC || af == AF_LINK) { + const char *reason; + + switch (ifp->carrier) { + case LINK_UP: + reason = "CARRIER"; + break; + case LINK_DOWN: + reason = "NOCARRIER"; + break; + default: + reason = "UNKNOWN"; + break; + } + if (fd != NULL) { + if (send_interface1(fd, ifp, reason) == -1) + retval = -1; + } else + retval++; + } + +#ifdef INET + if (af == AF_UNSPEC || af == AF_INET) { + if (D_STATE_RUNNING(ifp)) { + d = D_CSTATE(ifp); + if (fd != NULL) { + if (send_interface1(fd, ifp, d->reason) == -1) + retval = -1; + } else + retval++; + } +#ifdef IPV4LL + if (IPV4LL_STATE_RUNNING(ifp)) { + if (fd != NULL) { + if (send_interface1(fd, ifp, "IPV4LL") == -1) + retval = -1; + } else + retval++; + } +#endif + } +#endif + +#ifdef INET6 + if (af == AF_UNSPEC || af == AF_INET6) { + if (IPV6_STATE_RUNNING(ifp)) { + if (fd != NULL) { + if (send_interface1(fd, ifp, "STATIC6") == -1) + retval = -1; + } else + retval++; + } + if (RS_STATE_RUNNING(ifp)) { + if (fd != NULL) { + if (send_interface1(fd, ifp, + "ROUTERADVERT") == -1) + retval = -1; + } else + retval++; + } +#ifdef DHCP6 + if (D6_STATE_RUNNING(ifp)) { + d6 = D6_CSTATE(ifp); + if (fd != NULL) { + if (send_interface1(fd, ifp, d6->reason) == -1) + retval = -1; + } else + retval++; + } +#endif + } +#endif + + return retval; +} + +static int +script_run(struct dhcpcd_ctx *ctx, char **argv) +{ + pid_t pid; + int status = 0; + + pid = script_exec(argv, ctx->script_env); + if (pid == -1) + logerr("%s: %s", __func__, argv[0]); + else if (pid != 0) { + /* Wait for the script to finish */ + while (waitpid(pid, &status, 0) == -1) { + if (errno != EINTR) { + logerr("%s: waitpid", __func__); + status = 0; + break; + } + } + if (WIFEXITED(status)) { + if (WEXITSTATUS(status)) + logerrx("%s: %s: WEXITSTATUS %d", + __func__, argv[0], WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) + logerrx("%s: %s: %s", + __func__, argv[0], strsignal(WTERMSIG(status))); + } + + return WEXITSTATUS(status); +} + +int +script_dump(const char *env, size_t len) +{ + const char *ep = env + len; + + if (len == 0) + return 0; + + if (*(ep - 1) != '\0') { + errno = EINVAL; + return -1; + } + + for (; env < ep; env += strlen(env) + 1) { + if (strncmp(env, "new_", 4) == 0) + env += 4; + printf("%s\n", env); + } + return 0; +} + +int +script_runreason(const struct interface *ifp, const char *reason) +{ + struct dhcpcd_ctx *ctx = ifp->ctx; + char *argv[2]; + int status = 0; + struct fd_list *fd; + long buflen; + + if (ctx->script == NULL && + TAILQ_FIRST(&ifp->ctx->control_fds) == NULL) + return 0; + + /* Make our env */ + if ((buflen = make_env(ifp->ctx, ifp, reason)) == -1) { + logerr(__func__); + return -1; + } + + if (strncmp(reason, "DUMP", 4) == 0) + return script_dump(ctx->script_buf, (size_t)buflen); + + if (ctx->script == NULL) + goto send_listeners; + + argv[0] = ctx->script; + argv[1] = NULL; + logdebugx("%s: executing: %s %s", ifp->name, argv[0], reason); + +#ifdef PRIVSEP + if (ctx->options & DHCPCD_PRIVSEP) { + if (ps_root_script(ctx, + ctx->script_buf, ctx->script_buflen) == -1) + logerr(__func__); + goto send_listeners; + } +#endif + + script_run(ctx, argv); + +send_listeners: + /* Send to our listeners */ + status = 0; + TAILQ_FOREACH(fd, &ctx->control_fds, next) { + if (!(fd->flags & FD_LISTEN)) + continue; + if (control_queue(fd, ctx->script_buf, ctx->script_buflen)== -1) + logerr("%s: control_queue", __func__); + else + status = 1; + } + + return status; +} |