6fbf66fa |
/*
* 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.
* |
49979459 |
* Copyright (C) 2002-2018 OpenVPN Inc <sales@openvpn.net> |
f87b1bec |
* Copyright (C) 2013 David Sommerseth <davids@redhat.com> |
6fbf66fa |
*
* 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.
* |
caa54ac3 |
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
6fbf66fa |
*/
/*
* OpenVPN plugin module to do privileged down-script execution.
*/
|
ce8271f5 |
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
|
6fbf66fa |
#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> |
b0f2c521 |
#include <errno.h> |
7dd51f6f |
#include <err.h> |
6fbf66fa |
|
ce8271f5 |
#include <openvpn-plugin.h> |
6fbf66fa |
#define DEBUG(verb) ((verb) >= 7)
/* Command codes for foreground -> background communication */ |
f87b1bec |
#define COMMAND_RUN_SCRIPT 1
#define COMMAND_EXIT 2 |
6fbf66fa |
/* 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 */ |
81d882d5 |
static void down_root_server(const int fd, char *const *argv, char *const *envp, const int verb); |
6fbf66fa |
/*
* Plugin state, used by foreground
*/
struct down_root_context
{ |
e2e9a69c |
/* Foreground's socket to background process */
int foreground_fd; |
6fbf66fa |
|
e2e9a69c |
/* Process ID of background process */
pid_t background_pid; |
6fbf66fa |
|
e2e9a69c |
/* Verbosity level of OpenVPN */
int verb; |
6fbf66fa |
|
e2e9a69c |
/* down command */
char **command; |
6fbf66fa |
};
/*
* Given an environmental variable name, search
* the envp array for its value, returning it
* if found or NULL otherwise.
*/
static const char * |
81d882d5 |
get_env(const char *name, const char *envp[]) |
6fbf66fa |
{ |
e2e9a69c |
if (envp) |
6fbf66fa |
{ |
e2e9a69c |
int i; |
81d882d5 |
const int namelen = strlen(name); |
e2e9a69c |
for (i = 0; envp[i]; ++i)
{ |
81d882d5 |
if (!strncmp(envp[i], name, namelen)) |
e2e9a69c |
{
const char *cp = envp[i] + namelen;
if (*cp == '=') |
81d882d5 |
{ |
e2e9a69c |
return cp + 1; |
81d882d5 |
} |
e2e9a69c |
}
} |
6fbf66fa |
} |
e2e9a69c |
return NULL; |
6fbf66fa |
}
/*
* Return the length of a string array
*/
static int |
81d882d5 |
string_array_len(const char *array[]) |
6fbf66fa |
{ |
e2e9a69c |
int i = 0;
if (array) |
6fbf66fa |
{ |
e2e9a69c |
while (array[i]) |
4cd4899e |
{ |
e2e9a69c |
++i; |
4cd4899e |
} |
6fbf66fa |
} |
e2e9a69c |
return i; |
6fbf66fa |
}
/*
* Socket read/write functions.
*/
static int |
81d882d5 |
recv_control(int fd) |
6fbf66fa |
{ |
e2e9a69c |
unsigned char c; |
81d882d5 |
const ssize_t size = read(fd, &c, sizeof(c));
if (size == sizeof(c))
{ |
e2e9a69c |
return c; |
81d882d5 |
} |
e2e9a69c |
else |
81d882d5 |
{ |
e2e9a69c |
return -1; |
81d882d5 |
} |
6fbf66fa |
}
static int |
81d882d5 |
send_control(int fd, int code) |
6fbf66fa |
{ |
e2e9a69c |
unsigned char c = (unsigned char) code; |
81d882d5 |
const ssize_t size = write(fd, &c, sizeof(c));
if (size == sizeof(c))
{ |
e2e9a69c |
return (int) size; |
81d882d5 |
} |
e2e9a69c |
else |
81d882d5 |
{ |
e2e9a69c |
return -1; |
81d882d5 |
} |
6fbf66fa |
}
/*
* Daemonize if "daemon" env var is true.
* Preserve stderr across daemonization if
* "daemon_log_redirect" env var is true.
*/
static void |
81d882d5 |
daemonize(const char *envp[]) |
6fbf66fa |
{ |
81d882d5 |
const char *daemon_string = get_env("daemon", envp); |
e2e9a69c |
if (daemon_string && daemon_string[0] == '1') |
6fbf66fa |
{ |
81d882d5 |
const char *log_redirect = get_env("daemon_log_redirect", envp); |
e2e9a69c |
int fd = -1;
if (log_redirect && log_redirect[0] == '1')
{ |
81d882d5 |
fd = dup(2);
}
if (daemon(0, 0) < 0)
{
warn("DOWN-ROOT: daemonization failed"); |
e2e9a69c |
}
else if (fd >= 3)
{ |
81d882d5 |
dup2(fd, 2);
close(fd); |
e2e9a69c |
} |
6fbf66fa |
}
}
/*
* 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 |
81d882d5 |
close_fds_except(int keep) |
6fbf66fa |
{ |
e2e9a69c |
int i; |
81d882d5 |
closelog(); |
e2e9a69c |
for (i = 3; i <= 100; ++i) |
6fbf66fa |
{ |
e2e9a69c |
if (i != keep) |
81d882d5 |
{
close(i);
} |
6fbf66fa |
}
}
/*
* Usually we ignore signals, because our parent will
* deal with them.
*/
static void |
81d882d5 |
set_signals(void) |
6fbf66fa |
{ |
81d882d5 |
signal(SIGTERM, SIG_DFL); |
6fbf66fa |
|
81d882d5 |
signal(SIGINT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);
signal(SIGPIPE, SIG_IGN); |
6fbf66fa |
}
static void |
81d882d5 |
free_context(struct down_root_context *context) |
6fbf66fa |
{ |
e2e9a69c |
if (context) |
6fbf66fa |
{ |
e2e9a69c |
if (context->command)
{ |
81d882d5 |
free(context->command); |
e2e9a69c |
} |
81d882d5 |
free(context); |
6fbf66fa |
}
}
|
f87b1bec |
/* Run the script using execve(). As execve() replaces the
* current process with the new one, do a fork first before
* calling execve()
*/
static int |
81d882d5 |
run_script(char *const *argv, char *const *envp) |
e2e9a69c |
{
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]);
} |
81d882d5 |
else if (pid < (pid_t)0) |
e2e9a69c |
{ |
81d882d5 |
warn("DOWN-ROOT: Failed to fork child to run %s", argv[0]); |
e2e9a69c |
return -1;
}
else /* parent side */
{ |
81d882d5 |
if (waitpid(pid, &ret, 0) != pid) |
e2e9a69c |
{
/* 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;
} |
f87b1bec |
} |
e2e9a69c |
return ret; |
f87b1bec |
}
|
6fbf66fa |
OPENVPN_EXPORT openvpn_plugin_handle_t |
81d882d5 |
openvpn_plugin_open_v1(unsigned int *type_mask, const char *argv[], const char *envp[]) |
6fbf66fa |
{ |
e2e9a69c |
struct down_root_context *context;
int i = 0;
/*
* Allocate our context
*/ |
81d882d5 |
context = (struct down_root_context *) calloc(1, sizeof(struct down_root_context)); |
e2e9a69c |
if (!context) |
b0f2c521 |
{ |
81d882d5 |
warn("DOWN-ROOT: Could not allocate memory for plug-in context"); |
e2e9a69c |
goto error; |
b0f2c521 |
} |
e2e9a69c |
context->foreground_fd = -1;
/*
* Intercept the --up and --down callbacks
*/ |
81d882d5 |
*type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_UP) | OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_DOWN); |
e2e9a69c |
/*
* Make sure we have two string arguments: the first is the .so name,
* the second is the script command.
*/ |
81d882d5 |
if (string_array_len(argv) < 2) |
6fbf66fa |
{ |
81d882d5 |
fprintf(stderr, "DOWN-ROOT: need down script command\n"); |
e2e9a69c |
goto error; |
6fbf66fa |
}
|
e2e9a69c |
/*
* Save the arguments in our context
*/
context->command = calloc(string_array_len(argv), sizeof(char *));
if (!context->command) |
b0f2c521 |
{ |
81d882d5 |
warn("DOWN-ROOT: Could not allocate memory for command array"); |
e2e9a69c |
goto error; |
b0f2c521 |
}
|
e2e9a69c |
/* Ignore argv[0], as it contains just the plug-in file name */
for (i = 1; i < string_array_len(argv); i++) |
f87b1bec |
{ |
e2e9a69c |
context->command[i-1] = (char *) argv[i]; |
f87b1bec |
} |
6fbf66fa |
|
e2e9a69c |
/*
* Get verbosity level from environment
*/
{ |
81d882d5 |
const char *verb_string = get_env("verb", envp); |
e2e9a69c |
if (verb_string) |
81d882d5 |
{
context->verb = atoi(verb_string);
} |
e2e9a69c |
} |
6fbf66fa |
|
e2e9a69c |
return (openvpn_plugin_handle_t) context; |
6fbf66fa |
|
e2e9a69c |
error: |
81d882d5 |
free_context(context); |
e2e9a69c |
return NULL; |
6fbf66fa |
}
OPENVPN_EXPORT int |
81d882d5 |
openvpn_plugin_func_v1(openvpn_plugin_handle_t handle, const int type, const char *argv[], const char *envp[]) |
6fbf66fa |
{ |
e2e9a69c |
struct down_root_context *context = (struct down_root_context *) handle; |
6fbf66fa |
|
e2e9a69c |
if (type == OPENVPN_PLUGIN_UP && context->foreground_fd == -1) /* fork off a process to hold onto root */ |
6fbf66fa |
{ |
e2e9a69c |
pid_t pid;
int fd[2];
/*
* Make a socket for foreground and background processes
* to communicate.
*/ |
81d882d5 |
if (socketpair(PF_UNIX, SOCK_DGRAM, 0, fd) == -1) |
e2e9a69c |
{ |
81d882d5 |
warn("DOWN-ROOT: socketpair call failed"); |
e2e9a69c |
return OPENVPN_PLUGIN_FUNC_ERROR;
}
/*
* Fork off the privileged process. It will remain privileged
* even after the foreground process drops its privileges.
*/ |
81d882d5 |
pid = fork(); |
e2e9a69c |
if (pid)
{
int status;
/*
* Foreground Process
*/
context->background_pid = pid;
/* close our copy of child's socket */ |
81d882d5 |
close(fd[1]); |
e2e9a69c |
/* don't let future subprocesses inherit child socket */ |
81d882d5 |
if (fcntl(fd[0], F_SETFD, FD_CLOEXEC) < 0) |
e2e9a69c |
{ |
81d882d5 |
warn("DOWN-ROOT: Set FD_CLOEXEC flag on socket file descriptor failed"); |
e2e9a69c |
}
/* wait for background child process to initialize */ |
81d882d5 |
status = recv_control(fd[0]); |
e2e9a69c |
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 */ |
81d882d5 |
close_fds_except(fd[1]); |
e2e9a69c |
/* Ignore most signals (the parent will receive them) */ |
81d882d5 |
set_signals(); |
e2e9a69c |
/* Daemonize if --daemon option is set. */ |
81d882d5 |
daemonize(envp); |
e2e9a69c |
/* execute the event loop */ |
81d882d5 |
down_root_server(fd[1], context->command, (char *const *) envp, context->verb); |
e2e9a69c |
|
81d882d5 |
close(fd[1]);
exit(0); |
e2e9a69c |
return 0; /* NOTREACHED */
} |
6fbf66fa |
} |
e2e9a69c |
else if (type == OPENVPN_PLUGIN_DOWN && context->foreground_fd >= 0) |
6fbf66fa |
{ |
81d882d5 |
if (send_control(context->foreground_fd, COMMAND_RUN_SCRIPT) == -1) |
e2e9a69c |
{ |
81d882d5 |
warn("DOWN-ROOT: Error sending script execution signal to background process"); |
e2e9a69c |
}
else
{ |
81d882d5 |
const int status = recv_control(context->foreground_fd); |
e2e9a69c |
if (status == RESPONSE_SCRIPT_SUCCEEDED) |
81d882d5 |
{ |
e2e9a69c |
return OPENVPN_PLUGIN_FUNC_SUCCESS; |
81d882d5 |
} |
e2e9a69c |
if (status == -1)
{ |
81d882d5 |
warn("DOWN-ROOT: Error receiving script execution confirmation from background process"); |
e2e9a69c |
}
} |
6fbf66fa |
} |
e2e9a69c |
return OPENVPN_PLUGIN_FUNC_ERROR; |
6fbf66fa |
}
OPENVPN_EXPORT void |
81d882d5 |
openvpn_plugin_close_v1(openvpn_plugin_handle_t handle) |
6fbf66fa |
{ |
e2e9a69c |
struct down_root_context *context = (struct down_root_context *) handle; |
6fbf66fa |
|
81d882d5 |
if (DEBUG(context->verb))
{
fprintf(stderr, "DOWN-ROOT: close\n");
} |
6fbf66fa |
|
e2e9a69c |
if (context->foreground_fd >= 0) |
6fbf66fa |
{ |
e2e9a69c |
/* tell background process to exit */ |
81d882d5 |
if (send_control(context->foreground_fd, COMMAND_EXIT) == -1) |
e2e9a69c |
{ |
81d882d5 |
warn("DOWN-ROOT: Error signalling background process to exit"); |
e2e9a69c |
}
/* wait for background process to exit */
if (context->background_pid > 0) |
81d882d5 |
{
waitpid(context->background_pid, NULL, 0);
} |
e2e9a69c |
|
81d882d5 |
close(context->foreground_fd); |
e2e9a69c |
context->foreground_fd = -1; |
6fbf66fa |
}
|
81d882d5 |
free_context(context); |
6fbf66fa |
}
OPENVPN_EXPORT void |
81d882d5 |
openvpn_plugin_abort_v1(openvpn_plugin_handle_t handle) |
6fbf66fa |
{ |
e2e9a69c |
struct down_root_context *context = (struct down_root_context *) handle; |
6fbf66fa |
|
e2e9a69c |
if (context && context->foreground_fd >= 0) |
6fbf66fa |
{ |
e2e9a69c |
/* tell background process to exit */ |
81d882d5 |
send_control(context->foreground_fd, COMMAND_EXIT);
close(context->foreground_fd); |
e2e9a69c |
context->foreground_fd = -1; |
6fbf66fa |
}
}
/*
* Background process -- runs with privilege.
*/
static void |
81d882d5 |
down_root_server(const int fd, char *const *argv, char *const *envp, const int verb) |
6fbf66fa |
{ |
e2e9a69c |
/*
* Do initialization
*/ |
81d882d5 |
if (DEBUG(verb))
{
fprintf(stderr, "DOWN-ROOT: BACKGROUND: INIT command='%s'\n", argv[0]);
} |
e2e9a69c |
/*
* Tell foreground that we initialized successfully
*/ |
81d882d5 |
if (send_control(fd, RESPONSE_INIT_SUCCEEDED) == -1) |
6fbf66fa |
{ |
81d882d5 |
warn("DOWN-ROOT: BACKGROUND: write error on response socket [1]"); |
e2e9a69c |
goto done; |
6fbf66fa |
}
|
e2e9a69c |
/*
* Event loop
*/
while (1) |
6fbf66fa |
{ |
e2e9a69c |
int command_code;
int exit_code = -1;
/* get a command from foreground process */ |
81d882d5 |
command_code = recv_control(fd); |
e2e9a69c |
|
81d882d5 |
if (DEBUG(verb))
{
fprintf(stderr, "DOWN-ROOT: BACKGROUND: received command code: %d\n", command_code);
} |
e2e9a69c |
switch (command_code)
{ |
81d882d5 |
case COMMAND_RUN_SCRIPT:
if ( (exit_code = run_script(argv, envp)) == 0) /* Succeeded */ |
e2e9a69c |
{ |
81d882d5 |
if (send_control(fd, RESPONSE_SCRIPT_SUCCEEDED) == -1)
{
warn("DOWN-ROOT: BACKGROUND: write error on response socket [2]");
goto done;
} |
e2e9a69c |
} |
81d882d5 |
else /* Failed */ |
e2e9a69c |
{ |
81d882d5 |
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;
} |
e2e9a69c |
} |
81d882d5 |
break; |
e2e9a69c |
|
81d882d5 |
case COMMAND_EXIT:
goto done; |
e2e9a69c |
|
81d882d5 |
case -1:
warn("DOWN-ROOT: BACKGROUND: read error on command channel");
goto done; |
e2e9a69c |
|
81d882d5 |
default:
fprintf(stderr, "DOWN-ROOT: BACKGROUND: unknown command code: code=%d, exiting\n",
command_code);
goto done; |
e2e9a69c |
} |
6fbf66fa |
}
|
e2e9a69c |
done: |
81d882d5 |
if (DEBUG(verb))
{
fprintf(stderr, "DOWN-ROOT: BACKGROUND: EXIT\n");
} |
6fbf66fa |
|
e2e9a69c |
return; |
6fbf66fa |
} |
e2e9a69c |
/* |
81d882d5 |
* Local variables:
* c-file-style: "bsd"
* c-basic-offset: 4
* indent-tabs-mode: nil
* End:
*/ |