/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2026 OpenVPN Inc <sales@openvpn.net>
 *  Copyright (C) 2013      David Sommerseth <davids@redhat.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, see <https://www.gnu.org/licenses/>.
 */

/*
 * OpenVPN plugin module to do privileged down-script execution.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
#include <errno.h>
#include <err.h>

#include <openvpn-plugin.h>

#define DEBUG(verb) ((verb) >= 7)

/* Command codes for foreground -> background communication */
#define COMMAND_RUN_SCRIPT 1
#define COMMAND_EXIT       2

/* Response codes for background -> foreground communication */
#define RESPONSE_INIT_SUCCEEDED   10
#define RESPONSE_INIT_FAILED      11
#define RESPONSE_SCRIPT_SUCCEEDED 12
#define RESPONSE_SCRIPT_FAILED    13

/* Background process function */
static void down_root_server(const int fd, char *const *argv, char *const *envp, const int verb);

/*
 * Plugin state, used by foreground
 */
struct down_root_context
{
    /* Foreground's socket to background process */
    int foreground_fd;

    /* Process ID of background process */
    pid_t background_pid;

    /* Verbosity level of OpenVPN */
    int verb;

    /* down command */
    char **command;
};

/*
 * Given an environmental variable name, search
 * the envp array for its value, returning it
 * if found or NULL otherwise.
 */
static const char *
get_env(const char *name, const char *envp[])
{
    if (envp)
    {
        const size_t namelen = strlen(name);
        for (int i = 0; envp[i]; ++i)
        {
            if (!strncmp(envp[i], name, namelen))
            {
                const char *cp = envp[i] + namelen;
                if (*cp == '=')
                {
                    return cp + 1;
                }
            }
        }
    }
    return NULL;
}

/*
 * Return the length of a string array
 */
static size_t
string_array_len(const char *array[])
{
    size_t i = 0;
    if (array)
    {
        while (array[i])
        {
            ++i;
        }
    }
    return i;
}

/*
 * Socket read/write functions.
 */

static int
recv_control(int fd)
{
    unsigned char c;
    const ssize_t size = read(fd, &c, sizeof(c));
    if (size == sizeof(c))
    {
        return c;
    }
    else
    {
        return -1;
    }
}

static ssize_t
send_control(int fd, int code)
{
    unsigned char c = (unsigned char)code;
    const ssize_t size = write(fd, &c, sizeof(c));
    if (size == sizeof(c))
    {
        return size;
    }
    else
    {
        return -1;
    }
}

/*
 * Daemonize if "daemon" env var is true.
 * Preserve stderr across daemonization if
 * "daemon_log_redirect" env var is true.
 */
static void
daemonize(const char *envp[])
{
    const char *daemon_string = get_env("daemon", envp);
    if (daemon_string && daemon_string[0] == '1')
    {
        const char *log_redirect = get_env("daemon_log_redirect", envp);
        int fd = -1;
        if (log_redirect && log_redirect[0] == '1')
        {
            fd = dup(2);
        }
#if defined(__APPLE__) && defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
        if (daemon(0, 0) < 0)
        {
            warn("DOWN-ROOT: daemonization failed");
        }
#if defined(__APPLE__) && defined(__clang__)
#pragma clang diagnostic pop
#endif
        else if (fd >= 3)
        {
            dup2(fd, 2);
            close(fd);
        }
    }
}

/*
 * Close most of parent's fds.
 * Keep stdin/stdout/stderr, plus one
 * other fd which is presumed to be
 * our pipe back to parent.
 * Admittedly, a bit of a kludge,
 * but posix doesn't give us a kind
 * of FD_CLOEXEC which will stop
 * fds from crossing a fork().
 */
static void
close_fds_except(int keep)
{
    int i;
    closelog();
    for (i = 3; i <= 100; ++i)
    {
        if (i != keep)
        {
            close(i);
        }
    }
}

/*
 * Usually we ignore signals, because our parent will
 * deal with them.
 */
static void
set_signals(void)
{
    signal(SIGTERM, SIG_DFL);

    signal(SIGINT, SIG_IGN);
    signal(SIGHUP, SIG_IGN);
    signal(SIGUSR1, SIG_IGN);
    signal(SIGUSR2, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
}


static void
free_context(struct down_root_context *context)
{
    if (context)
    {
        free(context->command);
        free(context);
    }
}

/* Run the script using execve().  As execve() replaces the
 * current process with the new one, do a fork first before
 * calling execve()
 */
static int
run_script(char *const *argv, char *const *envp)
{
    pid_t pid;
    int ret = 0;

    pid = fork();
    if (pid == (pid_t)0) /* child side */
    {
        execve(argv[0], argv, envp);
        /* If execve() fails to run, exit child with exit code 127 */
        err(127, "DOWN-ROOT: Failed execute: %s", argv[0]);
    }
    else if (pid < (pid_t)0)
    {
        warn("DOWN-ROOT: Failed to fork child to run %s", argv[0]);
        return -1;
    }
    else /* parent side */
    {
        if (waitpid(pid, &ret, 0) != pid)
        {
            /* waitpid does not return error information via errno */
            fprintf(stderr, "DOWN-ROOT: waitpid() failed, don't know exit code of child (%s)\n",
                    argv[0]);
            return -1;
        }
    }
    return ret;
}

OPENVPN_EXPORT openvpn_plugin_handle_t
openvpn_plugin_open_v1(unsigned int *type_mask, const char *argv[], const char *envp[])
{
    struct down_root_context *context;

    /*
     * Allocate our context
     */
    context = (struct down_root_context *)calloc(1, sizeof(struct down_root_context));
    if (!context)
    {
        warn("DOWN-ROOT: Could not allocate memory for plug-in context");
        goto error;
    }
    context->foreground_fd = -1;

    /*
     * Intercept the --up and --down callbacks
     */
    *type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_UP) | OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_DOWN);

    /*
     * Make sure we have two string arguments: the first is the .so name,
     * the second is the script command.
     */
    if (string_array_len(argv) < 2)
    {
        fprintf(stderr, "DOWN-ROOT: need down script command\n");
        goto error;
    }

    /*
     * Save the arguments in our context
     */
    context->command = calloc(string_array_len(argv), sizeof(char *));
    if (!context->command)
    {
        warn("DOWN-ROOT: Could not allocate memory for command array");
        goto error;
    }

    /* Ignore argv[0], as it contains just the plug-in file name */
    for (size_t i = 1; i < string_array_len(argv); i++)
    {
        context->command[i - 1] = (char *)argv[i];
    }

    /*
     * Get verbosity level from environment
     */
    {
        const char *verb_string = get_env("verb", envp);
        if (verb_string)
        {
            context->verb = atoi(verb_string);
        }
    }

    return (openvpn_plugin_handle_t)context;

error:
    free_context(context);
    return NULL;
}

OPENVPN_EXPORT int
openvpn_plugin_func_v1(openvpn_plugin_handle_t handle, const int type, const char *argv[],
                       const char *envp[])
{
    struct down_root_context *context = (struct down_root_context *)handle;

    if (type == OPENVPN_PLUGIN_UP
        && context->foreground_fd == -1) /* fork off a process to hold onto root */
    {
        pid_t pid;
        int fd[2];

        /*
         * Make a socket for foreground and background processes
         * to communicate.
         */
        if (socketpair(PF_UNIX, SOCK_DGRAM, 0, fd) == -1)
        {
            warn("DOWN-ROOT: socketpair call failed");
            return OPENVPN_PLUGIN_FUNC_ERROR;
        }

        /*
         * Fork off the privileged process.  It will remain privileged
         * even after the foreground process drops its privileges.
         */
        pid = fork();

        if (pid)
        {
            int status;

            /*
             * Foreground Process
             */

            context->background_pid = pid;

            /* close our copy of child's socket */
            close(fd[1]);

            /* don't let future subprocesses inherit child socket */
            if (fcntl(fd[0], F_SETFD, FD_CLOEXEC) < 0)
            {
                warn("DOWN-ROOT: Set FD_CLOEXEC flag on socket file descriptor failed");
            }

            /* wait for background child process to initialize */
            status = recv_control(fd[0]);
            if (status == RESPONSE_INIT_SUCCEEDED)
            {
                context->foreground_fd = fd[0];
                return OPENVPN_PLUGIN_FUNC_SUCCESS;
            }
        }
        else
        {
            /*
             * Background Process
             */

            /* close all parent fds except our socket back to parent */
            close_fds_except(fd[1]);

            /* Ignore most signals (the parent will receive them) */
            set_signals();

            /* Daemonize if --daemon option is set. */
            daemonize(envp);

            /* execute the event loop */
            down_root_server(fd[1], context->command, (char *const *)envp, context->verb);

            close(fd[1]);
            exit(0);
            return 0; /* NOTREACHED */
        }
    }
    else if (type == OPENVPN_PLUGIN_DOWN && context->foreground_fd >= 0)
    {
        if (send_control(context->foreground_fd, COMMAND_RUN_SCRIPT) == -1)
        {
            warn("DOWN-ROOT: Error sending script execution signal to background process");
        }
        else
        {
            const int status = recv_control(context->foreground_fd);
            if (status == RESPONSE_SCRIPT_SUCCEEDED)
            {
                return OPENVPN_PLUGIN_FUNC_SUCCESS;
            }
            if (status == -1)
            {
                warn(
                    "DOWN-ROOT: Error receiving script execution confirmation from background process");
            }
        }
    }
    return OPENVPN_PLUGIN_FUNC_ERROR;
}

OPENVPN_EXPORT void
openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
{
    struct down_root_context *context = (struct down_root_context *)handle;

    if (DEBUG(context->verb))
    {
        fprintf(stderr, "DOWN-ROOT: close\n");
    }

    if (context->foreground_fd >= 0)
    {
        /* tell background process to exit */
        if (send_control(context->foreground_fd, COMMAND_EXIT) == -1)
        {
            warn("DOWN-ROOT: Error signalling background process to exit");
        }

        /* wait for background process to exit */
        if (context->background_pid > 0)
        {
            waitpid(context->background_pid, NULL, 0);
        }

        close(context->foreground_fd);
        context->foreground_fd = -1;
    }

    free_context(context);
}

OPENVPN_EXPORT void
openvpn_plugin_abort_v1(openvpn_plugin_handle_t handle)
{
    struct down_root_context *context = (struct down_root_context *)handle;

    if (context && context->foreground_fd >= 0)
    {
        /* tell background process to exit */
        send_control(context->foreground_fd, COMMAND_EXIT);
        close(context->foreground_fd);
        context->foreground_fd = -1;
    }
}

/*
 * Background process -- runs with privilege.
 */
static void
down_root_server(const int fd, char *const *argv, char *const *envp, const int verb)
{
    /*
     * Do initialization
     */
    if (DEBUG(verb))
    {
        fprintf(stderr, "DOWN-ROOT: BACKGROUND: INIT command='%s'\n", argv[0]);
    }

    /*
     * Tell foreground that we initialized successfully
     */
    if (send_control(fd, RESPONSE_INIT_SUCCEEDED) == -1)
    {
        warn("DOWN-ROOT: BACKGROUND: write error on response socket [1]");
        goto done;
    }

    /*
     * Event loop
     */
    while (1)
    {
        int command_code;
        int exit_code = -1;

        /* get a command from foreground process */
        command_code = recv_control(fd);

        if (DEBUG(verb))
        {
            fprintf(stderr, "DOWN-ROOT: BACKGROUND: received command code: %d\n", command_code);
        }

        switch (command_code)
        {
            case COMMAND_RUN_SCRIPT:
                if ((exit_code = run_script(argv, envp)) == 0) /* Succeeded */
                {
                    if (send_control(fd, RESPONSE_SCRIPT_SUCCEEDED) == -1)
                    {
                        warn("DOWN-ROOT: BACKGROUND: write error on response socket [2]");
                        goto done;
                    }
                }
                else /* Failed */
                {
                    fprintf(stderr, "DOWN-ROOT: BACKGROUND: %s exited with exit code %i\n", argv[0],
                            exit_code);
                    if (send_control(fd, RESPONSE_SCRIPT_FAILED) == -1)
                    {
                        warn("DOWN-ROOT: BACKGROUND: write error on response socket [3]");
                        goto done;
                    }
                }
                break;

            case COMMAND_EXIT:
                goto done;

            case -1:
                warn("DOWN-ROOT: BACKGROUND: read error on command channel");
                goto done;

            default:
                fprintf(stderr, "DOWN-ROOT: BACKGROUND: unknown command code: code=%d, exiting\n",
                        command_code);
                goto done;
        }
    }

done:
    if (DEBUG(verb))
    {
        fprintf(stderr, "DOWN-ROOT: BACKGROUND: EXIT\n");
    }

    return;
}


/*
 * Local variables:
 * c-file-style: "bsd"
 * c-basic-offset: 4
 * indent-tabs-mode: nil
 * End:
 */
