Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ out
.vs
.deps
.libs
.cache
Makefile
Makefile.in
aclocal.m4
Expand Down
29 changes: 29 additions & 0 deletions src/openvpn/manage.c
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ man_help(void)
msg(M_CLIENT, "push-update-broad options : Broadcast a message to update the specified options.");
msg(M_CLIENT, " Ex. push-update-broad \"route something, -dns\"");
msg(M_CLIENT, "push-update-cid CID options : Send an update message to the client identified by CID.");
msg(M_CLIENT, "reload-push-options [update-clients] : Reload push options from config file for new clients.");
msg(M_CLIENT, " With 'update-clients': also update connected clients (add new, remove old).");
msg(M_CLIENT, "END");
}

Expand Down Expand Up @@ -1723,6 +1725,33 @@ man_dispatch_command(struct management *man, struct status_output *so, const cha
man_push_update(man, p, UPT_BY_CID);
}
}
else if (streq(p[0], "reload-push-options"))
{
if (man->persist.callback.reload_push_options)
{
bool update_clients = (p[1] && streq(p[1], "update-clients"));
bool status = (*man->persist.callback.reload_push_options)(man->persist.callback.arg, update_clients);
if (status)
{
if (update_clients)
{
msg(M_CLIENT, "SUCCESS: push options reloaded and sent to all clients");
}
else
{
msg(M_CLIENT, "SUCCESS: push options reloaded from config file");
}
}
else
{
msg(M_CLIENT, "ERROR: failed to reload push options");
}
}
else
{
man_command_unsupported("reload-push-options");
}
}
#if 1
else if (streq(p[0], "test"))
{
Expand Down
3 changes: 2 additions & 1 deletion src/openvpn/manage.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
#include "socket_util.h"
#include "mroute.h"

#define MANAGEMENT_VERSION 5
#define MANAGEMENT_VERSION 6
#define MANAGEMENT_N_PASSWORD_RETRIES 3
#define MANAGEMENT_LOG_HISTORY_INITIAL_SIZE 100
#define MANAGEMENT_ECHO_BUFFER_SIZE 100
Expand Down Expand Up @@ -198,6 +198,7 @@ struct management_callback
bool (*remote_entry_get)(void *arg, unsigned int index, char **remote);
bool (*push_update_broadcast)(void *arg, const char *options);
bool (*push_update_by_cid)(void *arg, unsigned long cid, const char *options);
bool (*reload_push_options)(void *arg, bool update_clients);
};

/*
Expand Down
280 changes: 280 additions & 0 deletions src/openvpn/multi.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "forward.h"
#include "multi.h"
#include "push.h"
#include "options_util.h"
#include "run_command.h"
#include "otime.h"
#include "gremlin.h"
Expand Down Expand Up @@ -4100,6 +4101,284 @@ management_get_peer_info(void *arg, const unsigned long cid)
return ret;
}

/**
* Check if an option string exists in a push_list.
*/
static bool
push_option_exists(const struct push_list *list, const char *option)
{
const struct push_entry *e = list->head;
while (e)
{
if (e->enable && e->option && strcmp(e->option, option) == 0)
{
return true;
}
e = e->next;
}
return false;
}

/*
* Helper to append to push list using specific GC.
*/
static void
push_list_add(struct push_list *list, const char *opt, struct gc_arena *gc)
{
struct push_entry *e;
ALLOC_OBJ_CLEAR_GC(e, struct push_entry, gc);
e->enable = true;
e->option = opt;

if (list->tail)
{
list->tail->next = e;
list->tail = e;
}
else
{
list->head = e;
list->tail = e;
}
}

/**
* Find the index of an updatable option type for a given option string.
* @param option The option string to check (e.g., "route 10.0.0.0 255.0.0.0")
* @return Index into updatable_options[] or -1 if not found
*/
static ssize_t
find_updatable_option_index(const char *option)
{
size_t len = strlen(option);
for (size_t i = 0; i < updatable_options_count; ++i)
{
size_t opt_len = strlen(updatable_options[i]);
if (len >= opt_len
&& strncmp(option, updatable_options[i], opt_len) == 0
&& (option[opt_len] == '\0' || option[opt_len] == ' '))
{
return (ssize_t)i;
}
}
return -1;
}

/**
* Reload push options from the configuration file.
* This function re-reads the config file and updates the push_list
* that will be sent to new connecting clients.
*
* Thread safety: OpenVPN uses a single-threaded event loop, so this
* function runs sequentially with all other operations.
*
* @param arg Pointer to multi_context
* @param update_clients If true, update connected clients (add new, remove old)
* @return true on success, false on failure
*/
static bool
management_callback_reload_push_options(void *arg, bool update_clients)
{
struct multi_context *m = (struct multi_context *)arg;
struct gc_arena gc = gc_new();
bool ret = false;

msg(M_INFO, "MANAGEMENT: Reloading push options from config file");

/* Check if we have a config file to reload from */
if (!m->top.options.config)
{
msg(M_WARN, "MANAGEMENT: Cannot reload push options - no config file specified");
goto cleanup;
}

/* Save reference to old push_list for update comparison */
struct push_list old_push_list = m->top.options.push_list;

/* Create a temporary options structure to parse the config */
struct options new_options;
CLEAR(new_options);

/* Initialize the gc_arena for the new options */
new_options.gc = gc_new();

/* Set up environment for config parsing */
struct env_set *es = env_set_create(&gc);
unsigned int option_types_found = 0;

/* Re-read the configuration file */
read_config_file(&new_options, m->top.options.config, 0,
m->top.options.config, 0, M_WARN,
OPT_P_DEFAULT, &option_types_found, es);

/* Validate we got a sensible result - if old list had entries but new is empty,
* this likely indicates a parsing error */
if (old_push_list.head && !new_options.push_list.head)
{
msg(M_WARN, "MANAGEMENT: Config reload returned empty push list - aborting");
gc_free(&new_options.gc);
goto cleanup;
}

/* Create a new GC arena for the new push list */
struct gc_arena new_push_list_gc = gc_new();
struct push_list new_push_list = { NULL, NULL };

/* Copy each push entry from the parsed config to the new push_list
* using the new dedicated push_list_gc */
const struct push_entry *e = new_options.push_list.head;
while (e)
{
if (e->enable && e->option)
{
/* Copy the option string to the new dedicated gc_arena */
const char *opt = string_alloc(e->option, &new_push_list_gc);
push_list_add(&new_push_list, opt, &new_push_list_gc);
}
e = e->next;
}

/* Free the temporary options gc_arena (parsed config) */
gc_free(&new_options.gc);

/* Update connected clients if requested */
/* We do this BEFORE swapping the lists so we can compare old vs new */
if (update_clients)
{
/* Calculate required buffer size: sum of all option lengths + separators */
size_t opts_size = 0;
const struct push_entry *size_e = new_push_list.head;
while (size_e)
{
if (size_e->enable && size_e->option)
{
opts_size += strlen(size_e->option) + 2; /* option + ", " */
}
size_e = size_e->next;
}
/* Add space for removal commands: "-type, " for each updatable option type */
opts_size += updatable_options_count * 32;
/* Minimum size to avoid edge cases */
if (opts_size < PUSH_BUNDLE_SIZE)
{
opts_size = PUSH_BUNDLE_SIZE;
}

struct buffer opts = alloc_buf_gc(opts_size, &gc);
bool first = true;
int added = 0, removed = 0;

/* Set of option types that have been removed/modified */
bool *type_removed = gc_malloc(updatable_options_count * sizeof(bool), true, &gc);

/* 1. Detect removed options and mark their types */
const struct push_entry *old_e = old_push_list.head;
while (old_e)
{
if (old_e->enable && old_e->option)
{
if (!push_option_exists(&new_push_list, old_e->option))
{
ssize_t type_idx = find_updatable_option_index(old_e->option);
if (type_idx >= 0)
{
type_removed[type_idx] = true;
removed++;
msg(D_PUSH, "MANAGEMENT: Removing: %s", old_e->option);
}
else
{
msg(M_WARN, "MANAGEMENT: Cannot remove option '%s' (not updatable)", old_e->option);
}
}
}
old_e = old_e->next;
}

/* 2. Add removal commands for all marked types */
for (size_t i = 0; i < updatable_options_count; ++i)
{
if (type_removed[i])
{
if (!first)
{
buf_printf(&opts, ", ");
}
/* Send -type to remove all options of that type */
buf_printf(&opts, "-%s", updatable_options[i]);
first = false;
}
}

/* 3. Add new options AND re-add options belonging to removed types */
const struct push_entry *new_e = new_push_list.head;
while (new_e)
{
if (new_e->enable && new_e->option)
{
bool should_send = false;
bool is_existing = push_option_exists(&old_push_list, new_e->option);

/* Check if this option belongs to a type that was reset */
bool type_was_reset = false;
ssize_t type_idx = find_updatable_option_index(new_e->option);
if (type_idx >= 0 && type_removed[type_idx])
{
type_was_reset = true;
}

/* Always send new options */
if (!is_existing)
{
should_send = true;
added++;
msg(D_PUSH, "MANAGEMENT: Adding: %s", new_e->option);
}
/* Also resend options if their type was reset (because we sent -type) */
else if (type_was_reset)
{
should_send = true;
msg(D_PUSH, "MANAGEMENT: Re-adding (type reset): %s", new_e->option);
}

if (should_send)
{
if (!first)
{
buf_printf(&opts, ", ");
}
buf_printf(&opts, "%s", new_e->option);
first = false;
}
}
new_e = new_e->next;
}

if (BLEN(&opts) > 0)
{
msg(M_INFO, "MANAGEMENT: Updating clients with push options (added=%d, removed=%d)",
added, removed);
management_callback_send_push_update_broadcast(m, BSTR(&opts));
}
else
{
msg(M_INFO, "MANAGEMENT: No changes to send to clients");
}
}

/* Now replace the old push_list with the new one and free old memory */
gc_free(&m->top.options.push_list_gc);
m->top.options.push_list_gc = new_push_list_gc;
m->top.options.push_list = new_push_list;

msg(M_INFO, "MANAGEMENT: Push options reloaded successfully");
ret = true;

cleanup:
gc_free(&gc);
return ret;
}

#endif /* ifdef ENABLE_MANAGEMENT */


Expand All @@ -4125,6 +4404,7 @@ init_management_callback_multi(struct multi_context *m)
cb.get_peer_info = management_get_peer_info;
cb.push_update_broadcast = management_callback_send_push_update_broadcast;
cb.push_update_by_cid = management_callback_send_push_update_by_cid;
cb.reload_push_options = management_callback_reload_push_options;
management_set_callback(management, &cb);
}
#endif /* ifdef ENABLE_MANAGEMENT */
Expand Down
Loading