dns: support running up/down command with privsep

With --user privileges are dropped after init. Unfortunately this
affects --dns-updown when undoing previous modifications.

To keep the privileges for just that, the concept of a dns updown runner
in introduced. It's basically a fork of openvpn at the time the
modifications to DNS are made. Its only capability is running the
--dns-updown command when asked to. The parent openvpn process signals
this by writing to a pipe the runner is waiting on.

Commands need to be ready to receive variables from a file instead of the
process environment. A shameless and effective workaround to keep the
protocol between the two processes simple.

Change-Id: I6b67e3a00dd84bf348b6af28115ee11138c3a111
Signed-off-by: Heiko Hund <heiko@ist.eigentlich.net>
Acked-by: Gert Doering <gert@greenie.muc.de>
Message-Id: <20250517083833.28728-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg31668.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
This commit is contained in:
Heiko Hund 2025-05-17 10:38:27 +02:00 committed by Gert Doering
parent 6c33e3761e
commit 1dfe8729f6
10 changed files with 246 additions and 10 deletions

View file

@ -7,6 +7,10 @@
#
# Example env from openvpn (most are not applied):
#
# dns_vars_file /tmp/openvpn_dvf_58b95c0c97b2db43afb5d745f986c53c.tmp
#
# or
#
# dev tun0
# script_type dns-up
# dns_search_domain_1 mycorp.in
@ -39,6 +43,7 @@ conly_standard_server_ports() {
conf=/boot/system/settings/network/resolv.conf
test -e "$conf" || exit 1
test -z "${dns_vars_file}" || . "${dns_vars_file}"
case "${script_type}" in
dns-up)
n=1

View file

@ -8,6 +8,10 @@
#
# Example env from openvpn (most are not applied):
#
# dns_vars_file /tmp/openvpn_dvf_58b95c0c97b2db43afb5d745f986c53c.tmp
#
# or
#
# dev tun0
# script_type dns-up
# dns_search_domain_1 mycorp.in
@ -38,6 +42,7 @@ only_standard_server_ports() {
done
}
[ -z "${dns_vars_file}" ] || . "${dns_vars_file}"
: ${script_type:=dns-down}
case "${script_type}" in
dns-up)

View file

@ -7,6 +7,10 @@
#
# Example env from openvpn (most are not applied):
#
# dns_vars_file /tmp/openvpn_dvf_58b95c0c97b2db43afb5d745f986c53c.tmp
#
# or
#
# dev tun0
# script_type dns-up
# dns_search_domain_1 mycorp.in
@ -39,6 +43,7 @@ only_standard_server_ports() {
conf=/etc/resolv.conf
test -e "$conf" || exit 1
test -z "${dns_vars_file}" || . "${dns_vars_file}"
case "${script_type}" in
dns-up)
n=1

View file

@ -15,6 +15,10 @@
#
# Example env from openvpn (not all are always applied):
#
# dns_vars_file /tmp/openvpn_dvf_58b95c0c97b2db43afb5d745f986c53c.tmp
#
# or
#
# dev tun0
# script_type dns-up
# dns_search_domain_1 mycorp.in
@ -30,6 +34,8 @@
# dns_server_1_sni dns.mycorp.in
#
[ -z "${dns_vars_file}" ] || . "${dns_vars_file}"
function do_resolved_servers {
local sni=""
local transport_var=dns_server_${n}_transport

View file

@ -562,13 +562,20 @@ updown_env_set(bool up, const struct dns_options *o, const struct tuntap *tt, st
}
static int
do_run_up_down_command(bool up, const struct dns_options *o, const struct tuntap *tt)
do_run_up_down_command(bool up, const char *vars_file, const struct dns_options *o, const struct tuntap *tt)
{
struct gc_arena gc = gc_new();
struct argv argv = argv_new();
struct env_set *es = env_set_create(&gc);
updown_env_set(up, o, tt, es);
if (vars_file)
{
setenv_str(es, "dns_vars_file", vars_file);
}
else
{
updown_env_set(up, o, tt, es);
}
argv_printf(&argv, "%s", o->updown);
argv_msg(M_INFO, &argv);
@ -586,8 +593,115 @@ do_run_up_down_command(bool up, const struct dns_options *o, const struct tuntap
return res;
}
static bool
run_updown_runner(bool up, struct options *o, const struct tuntap *tt, struct dns_updown_runner_info *updown_runner)
{
int dns_pipe_fd[2];
int ack_pipe_fd[2];
if (pipe(dns_pipe_fd) != 0
|| pipe(ack_pipe_fd) != 0)
{
msg(M_ERR | M_ERRNO, "run_dns_up_down: unable to create pipes");
return false;
}
updown_runner->pid = fork();
if (updown_runner->pid == -1)
{
msg(M_ERR | M_ERRNO, "run_dns_up_down: unable to fork");
close(dns_pipe_fd[0]);
close(dns_pipe_fd[1]);
close(ack_pipe_fd[0]);
close(ack_pipe_fd[1]);
return false;
}
else if (updown_runner->pid > 0)
{
/* Parent process */
close(dns_pipe_fd[0]);
close(ack_pipe_fd[1]);
updown_runner->fds[0] = ack_pipe_fd[0];
updown_runner->fds[1] = dns_pipe_fd[1];
}
else
{
/* Script runner process, close unused FDs */
for (int fd = 3; fd < 100; ++fd)
{
if (fd != dns_pipe_fd[0]
&& fd != ack_pipe_fd[1])
{
close(fd);
}
}
/* Ignore signals */
signal(SIGINT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
signal(SIGTERM, SIG_IGN);
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
while (1)
{
ssize_t rlen, wlen;
char path[PATH_MAX];
/* Block here until parent sends a path */
rlen = read(dns_pipe_fd[0], &path, sizeof(path));
if (rlen < 1)
{
if (rlen == -1 && errno == EINTR)
{
continue;
}
close(dns_pipe_fd[0]);
close(ack_pipe_fd[1]);
exit(0);
}
path[sizeof(path) - 1] = '\0';
int res = do_run_up_down_command(up, path, &o->dns_options, tt);
platform_unlink(path);
/* Unblock parent process */
while (1)
{
wlen = write(ack_pipe_fd[1], &res, sizeof(res));
if ((wlen == -1 && errno != EINTR) || wlen < sizeof(res))
{
/* Not much we can do about errors but exit */
close(dns_pipe_fd[0]);
close(ack_pipe_fd[1]);
exit(0);
}
else if (wlen == sizeof(res))
{
break;
}
}
up = !up; /* do the opposite next time */
}
}
return true;
}
static const char *
write_dns_vars_file(bool up, const struct options *o, const struct tuntap *tt, struct gc_arena *gc)
{
struct env_set *es = env_set_create(gc);
const char *dvf = platform_create_temp_file(o->tmp_dir, "dvf", gc);
updown_env_set(up, &o->dns_options, tt, es);
env_set_write_file(dvf, es);
return dvf;
}
static void
run_up_down_command(bool up, struct options *o, const struct tuntap *tt)
run_up_down_command(bool up, struct options *o, const struct tuntap *tt, struct dns_updown_runner_info *updown_runner)
{
if (!o->dns_options.updown)
{
@ -595,7 +709,60 @@ run_up_down_command(bool up, struct options *o, const struct tuntap *tt)
}
int status;
status = do_run_up_down_command(up, &o->dns_options, tt);
if (!updown_runner->required)
{
/* Run dns updown directly */
status = do_run_up_down_command(up, NULL, &o->dns_options, tt);
}
else
{
if (updown_runner->pid < 1)
{
/* Need to set up privilege preserving child first */
if (!run_updown_runner(up, o, tt, updown_runner))
{
return;
}
}
struct gc_arena gc = gc_new();
int rfd = updown_runner->fds[0];
int wfd = updown_runner->fds[1];
const char *dvf = write_dns_vars_file(up, o, tt, &gc);
size_t dvf_size = strlen(dvf) + 1;
while (1)
{
ssize_t len = write(wfd, dvf, dvf_size);
if (len < dvf_size)
{
if (len == -1 && errno == EINTR)
{
continue;
}
msg(M_ERR | M_ERRNO, "could not send dns vars filename");
}
break;
}
while (1)
{
ssize_t len = read(rfd, &status, sizeof(status));
if (len < sizeof(status))
{
if (len == -1 && errno == EINTR)
{
continue;
}
msg(M_ERR | M_ERRNO, "could not receive dns updown status");
}
break;
}
gc_free(&gc);
}
msg(M_INFO, "dns %s command exited with status %d", up ? "up" : "down", status);
}
@ -681,7 +848,7 @@ show_dns_options(const struct dns_options *o)
}
void
run_dns_up_down(bool up, struct options *o, const struct tuntap *tt)
run_dns_up_down(bool up, struct options *o, const struct tuntap *tt, struct dns_updown_runner_info *duri)
{
if (!o->dns_options.servers)
{
@ -718,6 +885,6 @@ run_dns_up_down(bool up, struct options *o, const struct tuntap *tt)
#ifdef _WIN32
run_up_down_service(up, o, tt);
#else
run_up_down_command(up, o, tt);
run_up_down_command(up, o, tt, duri);
#endif /* ifdef _WIN32 */
}

View file

@ -68,6 +68,14 @@ struct dns_server {
const char *sni;
};
struct dns_updown_runner_info {
bool required;
int fds[2];
#if !defined(_WIN32)
pid_t pid;
#endif
};
struct dns_options {
struct dns_domain *search_domains;
struct dns_server *servers_prepull;
@ -154,8 +162,10 @@ void dns_options_postprocess_pull(struct dns_options *o);
* @param up Boolean to set this call to "up" when true
* @param o Pointer to the program options
* @param tt Pointer to the connection's tuntap struct
* @param duri Pointer to the updown runner info struct
*/
void run_dns_up_down(bool up, struct options *o, const struct tuntap *tt);
void run_dns_up_down(bool up, struct options *o, const struct tuntap *tt,
struct dns_updown_runner_info *duri);
/**
* Puts the DNS options into an environment set.

View file

@ -33,6 +33,7 @@
#include "env_set.h"
#include "run_command.h"
#include "platform.h"
/*
* Set environmental variable (int or string).
@ -234,6 +235,30 @@ env_set_print(int msglevel, const struct env_set *es)
}
}
void
env_set_write_file(const char *path, const struct env_set *es)
{
FILE *fp = platform_fopen(path, "w");
if (!fp)
{
msg(M_ERR, "could not write env set to '%s'", path);
return;
}
if (es)
{
const struct env_item *item = es->list;
while (item)
{
fputs(item->string, fp);
fputc('\n', fp);
item = item->next;
}
}
fclose(fp);
}
void
env_set_inherit(struct env_set *es, const struct env_set *src)
{

View file

@ -91,6 +91,14 @@ const char *env_set_get(const struct env_set *es, const char *name);
void env_set_print(int msglevel, const struct env_set *es);
/**
* Write a struct env_set to a file. Each item on one line.
*
* @param path The filepath to write to.
* @param es Pointer to the env_set to write.
*/
void env_set_write_file(const char *path, const struct env_set *es);
void env_set_inherit(struct env_set *es, const struct env_set *src);
/* returns true if environmental variable name starts with 'password' */

View file

@ -2027,7 +2027,7 @@ do_open_tun(struct context *c, int *error_flags)
c->c2.frame.tun_mtu, c->c2.es, &c->net_ctx);
}
run_dns_up_down(true, &c->options, c->c1.tuntap);
run_dns_up_down(true, &c->options, c->c1.tuntap, &c->persist.duri);
/* run the up script */
run_up_down(c->options.up_script,
@ -2067,7 +2067,7 @@ do_open_tun(struct context *c, int *error_flags)
/* explicitly set the ifconfig_* env vars */
do_ifconfig_setenv(c->c1.tuntap, c->c2.es);
run_dns_up_down(true, &c->options, c->c1.tuntap);
run_dns_up_down(true, &c->options, c->c1.tuntap, &c->persist.duri);
/* run the up script if user specified --up-restart */
if (c->options.up_restart)
@ -2157,7 +2157,7 @@ do_close_tun(struct context *c, bool force)
adapter_index = c->c1.tuntap->adapter_index;
#endif
run_dns_up_down(false, &c->options, c->c1.tuntap);
run_dns_up_down(false, &c->options, c->c1.tuntap, &c->persist.duri);
if (force || !(c->sig->signal_received == SIGUSR1 && c->options.persist_tun))
{
@ -3971,6 +3971,9 @@ do_init_first_time(struct context *c)
c0->uid_gid_specified = user_defined || group_defined;
/* fork the dns script runner to preserve root? */
c->persist.duri.required = user_defined;
/* perform postponed chdir if --daemon */
if (c->did_we_daemonize && c->options.cd_dir == NULL)
{

View file

@ -45,6 +45,7 @@
#include "pool.h"
#include "plugin.h"
#include "manage.h"
#include "dns.h"
/*
* Our global key schedules, packaged thusly
@ -120,6 +121,7 @@ struct context_buffers
struct context_persist
{
int restart_sleep_seconds;
struct dns_updown_runner_info duri;
};