Skip to content

Commit c752de1

Browse files
committed
Management: add reload-push-options command
This adds a new management command 'reload-push-options' that allows reloading the push options from the configuration file without restarting the server. This is useful for updating routes or DNS settings for new clients without dropping existing connections. The command supports an optional 'update-clients' argument. When provided, the server will also synchronize the new options to currently connected clients by: 1. Calculating the difference between old and new push options. 2. Sending '-instruction' (e.g. -route) to remove old options. 3. Sending new options via PUSH_UPDATE. This includes a comprehensive integration test suite in tests/reload_push_options.
1 parent dd1524c commit c752de1

20 files changed

Lines changed: 980 additions & 4 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ out
1515
.vs
1616
.deps
1717
.libs
18+
.cache
1819
Makefile
1920
Makefile.in
2021
aclocal.m4

src/openvpn/manage.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ man_help(void)
137137
msg(M_CLIENT, "push-update-broad options : Broadcast a message to update the specified options.");
138138
msg(M_CLIENT, " Ex. push-update-broad \"route something, -dns\"");
139139
msg(M_CLIENT, "push-update-cid CID options : Send an update message to the client identified by CID.");
140+
msg(M_CLIENT, "reload-push-options [update-clients] : Reload push options from config file for new clients.");
141+
msg(M_CLIENT, " With 'update-clients': also update connected clients (add new, remove old).");
140142
msg(M_CLIENT, "END");
141143
}
142144

@@ -1723,6 +1725,33 @@ man_dispatch_command(struct management *man, struct status_output *so, const cha
17231725
man_push_update(man, p, UPT_BY_CID);
17241726
}
17251727
}
1728+
else if (streq(p[0], "reload-push-options"))
1729+
{
1730+
if (man->persist.callback.reload_push_options)
1731+
{
1732+
bool update_clients = (p[1] && streq(p[1], "update-clients"));
1733+
bool status = (*man->persist.callback.reload_push_options)(man->persist.callback.arg, update_clients);
1734+
if (status)
1735+
{
1736+
if (update_clients)
1737+
{
1738+
msg(M_CLIENT, "SUCCESS: push options reloaded and sent to all clients");
1739+
}
1740+
else
1741+
{
1742+
msg(M_CLIENT, "SUCCESS: push options reloaded from config file");
1743+
}
1744+
}
1745+
else
1746+
{
1747+
msg(M_CLIENT, "ERROR: failed to reload push options");
1748+
}
1749+
}
1750+
else
1751+
{
1752+
man_command_unsupported("reload-push-options");
1753+
}
1754+
}
17261755
#if 1
17271756
else if (streq(p[0], "test"))
17281757
{

src/openvpn/manage.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
#include "socket_util.h"
5151
#include "mroute.h"
5252

53-
#define MANAGEMENT_VERSION 5
53+
#define MANAGEMENT_VERSION 6
5454
#define MANAGEMENT_N_PASSWORD_RETRIES 3
5555
#define MANAGEMENT_LOG_HISTORY_INITIAL_SIZE 100
5656
#define MANAGEMENT_ECHO_BUFFER_SIZE 100
@@ -198,6 +198,7 @@ struct management_callback
198198
bool (*remote_entry_get)(void *arg, unsigned int index, char **remote);
199199
bool (*push_update_broadcast)(void *arg, const char *options);
200200
bool (*push_update_by_cid)(void *arg, unsigned long cid, const char *options);
201+
bool (*reload_push_options)(void *arg, bool update_clients);
201202
};
202203

203204
/*

src/openvpn/multi.c

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include "forward.h"
3535
#include "multi.h"
3636
#include "push.h"
37+
#include "options_util.h"
3738
#include "run_command.h"
3839
#include "otime.h"
3940
#include "gremlin.h"
@@ -4100,6 +4101,284 @@ management_get_peer_info(void *arg, const unsigned long cid)
41004101
return ret;
41014102
}
41024103

4104+
/**
4105+
* Check if an option string exists in a push_list.
4106+
*/
4107+
static bool
4108+
push_option_exists(const struct push_list *list, const char *option)
4109+
{
4110+
const struct push_entry *e = list->head;
4111+
while (e)
4112+
{
4113+
if (e->enable && e->option && strcmp(e->option, option) == 0)
4114+
{
4115+
return true;
4116+
}
4117+
e = e->next;
4118+
}
4119+
return false;
4120+
}
4121+
4122+
/*
4123+
* Helper to append to push list using specific GC.
4124+
*/
4125+
static void
4126+
push_list_add(struct push_list *list, const char *opt, struct gc_arena *gc)
4127+
{
4128+
struct push_entry *e;
4129+
ALLOC_OBJ_CLEAR_GC(e, struct push_entry, gc);
4130+
e->enable = true;
4131+
e->option = opt;
4132+
4133+
if (list->tail)
4134+
{
4135+
list->tail->next = e;
4136+
list->tail = e;
4137+
}
4138+
else
4139+
{
4140+
list->head = e;
4141+
list->tail = e;
4142+
}
4143+
}
4144+
4145+
/**
4146+
* Find the index of an updatable option type for a given option string.
4147+
* @param option The option string to check (e.g., "route 10.0.0.0 255.0.0.0")
4148+
* @return Index into updatable_options[] or -1 if not found
4149+
*/
4150+
static ssize_t
4151+
find_updatable_option_index(const char *option)
4152+
{
4153+
size_t len = strlen(option);
4154+
for (size_t i = 0; i < updatable_options_count; ++i)
4155+
{
4156+
size_t opt_len = strlen(updatable_options[i]);
4157+
if (len >= opt_len
4158+
&& strncmp(option, updatable_options[i], opt_len) == 0
4159+
&& (option[opt_len] == '\0' || option[opt_len] == ' '))
4160+
{
4161+
return (ssize_t)i;
4162+
}
4163+
}
4164+
return -1;
4165+
}
4166+
4167+
/**
4168+
* Reload push options from the configuration file.
4169+
* This function re-reads the config file and updates the push_list
4170+
* that will be sent to new connecting clients.
4171+
*
4172+
* Thread safety: OpenVPN uses a single-threaded event loop, so this
4173+
* function runs sequentially with all other operations.
4174+
*
4175+
* @param arg Pointer to multi_context
4176+
* @param update_clients If true, update connected clients (add new, remove old)
4177+
* @return true on success, false on failure
4178+
*/
4179+
static bool
4180+
management_callback_reload_push_options(void *arg, bool update_clients)
4181+
{
4182+
struct multi_context *m = (struct multi_context *)arg;
4183+
struct gc_arena gc = gc_new();
4184+
bool ret = false;
4185+
4186+
msg(M_INFO, "MANAGEMENT: Reloading push options from config file");
4187+
4188+
/* Check if we have a config file to reload from */
4189+
if (!m->top.options.config)
4190+
{
4191+
msg(M_WARN, "MANAGEMENT: Cannot reload push options - no config file specified");
4192+
goto cleanup;
4193+
}
4194+
4195+
/* Save reference to old push_list for update comparison */
4196+
struct push_list old_push_list = m->top.options.push_list;
4197+
4198+
/* Create a temporary options structure to parse the config */
4199+
struct options new_options;
4200+
CLEAR(new_options);
4201+
4202+
/* Initialize the gc_arena for the new options */
4203+
new_options.gc = gc_new();
4204+
4205+
/* Set up environment for config parsing */
4206+
struct env_set *es = env_set_create(&gc);
4207+
unsigned int option_types_found = 0;
4208+
4209+
/* Re-read the configuration file */
4210+
read_config_file(&new_options, m->top.options.config, 0,
4211+
m->top.options.config, 0, M_WARN,
4212+
OPT_P_DEFAULT, &option_types_found, es);
4213+
4214+
/* Validate we got a sensible result - if old list had entries but new is empty,
4215+
* this likely indicates a parsing error */
4216+
if (old_push_list.head && !new_options.push_list.head)
4217+
{
4218+
msg(M_WARN, "MANAGEMENT: Config reload returned empty push list - aborting");
4219+
gc_free(&new_options.gc);
4220+
goto cleanup;
4221+
}
4222+
4223+
/* Create a new GC arena for the new push list */
4224+
struct gc_arena new_push_list_gc = gc_new();
4225+
struct push_list new_push_list = { NULL, NULL };
4226+
4227+
/* Copy each push entry from the parsed config to the new push_list
4228+
* using the new dedicated push_list_gc */
4229+
const struct push_entry *e = new_options.push_list.head;
4230+
while (e)
4231+
{
4232+
if (e->enable && e->option)
4233+
{
4234+
/* Copy the option string to the new dedicated gc_arena */
4235+
const char *opt = string_alloc(e->option, &new_push_list_gc);
4236+
push_list_add(&new_push_list, opt, &new_push_list_gc);
4237+
}
4238+
e = e->next;
4239+
}
4240+
4241+
/* Free the temporary options gc_arena (parsed config) */
4242+
gc_free(&new_options.gc);
4243+
4244+
/* Update connected clients if requested */
4245+
/* We do this BEFORE swapping the lists so we can compare old vs new */
4246+
if (update_clients)
4247+
{
4248+
/* Calculate required buffer size: sum of all option lengths + separators */
4249+
size_t opts_size = 0;
4250+
const struct push_entry *size_e = new_push_list.head;
4251+
while (size_e)
4252+
{
4253+
if (size_e->enable && size_e->option)
4254+
{
4255+
opts_size += strlen(size_e->option) + 2; /* option + ", " */
4256+
}
4257+
size_e = size_e->next;
4258+
}
4259+
/* Add space for removal commands: "-type, " for each updatable option type */
4260+
opts_size += updatable_options_count * 32;
4261+
/* Minimum size to avoid edge cases */
4262+
if (opts_size < PUSH_BUNDLE_SIZE)
4263+
{
4264+
opts_size = PUSH_BUNDLE_SIZE;
4265+
}
4266+
4267+
struct buffer opts = alloc_buf_gc(opts_size, &gc);
4268+
bool first = true;
4269+
int added = 0, removed = 0;
4270+
4271+
/* Set of option types that have been removed/modified */
4272+
bool *type_removed = gc_malloc(updatable_options_count * sizeof(bool), true, &gc);
4273+
4274+
/* 1. Detect removed options and mark their types */
4275+
const struct push_entry *old_e = old_push_list.head;
4276+
while (old_e)
4277+
{
4278+
if (old_e->enable && old_e->option)
4279+
{
4280+
if (!push_option_exists(&new_push_list, old_e->option))
4281+
{
4282+
ssize_t type_idx = find_updatable_option_index(old_e->option);
4283+
if (type_idx >= 0)
4284+
{
4285+
type_removed[type_idx] = true;
4286+
removed++;
4287+
msg(D_PUSH, "MANAGEMENT: Removing: %s", old_e->option);
4288+
}
4289+
else
4290+
{
4291+
msg(M_WARN, "MANAGEMENT: Cannot remove option '%s' (not updatable)", old_e->option);
4292+
}
4293+
}
4294+
}
4295+
old_e = old_e->next;
4296+
}
4297+
4298+
/* 2. Add removal commands for all marked types */
4299+
for (size_t i = 0; i < updatable_options_count; ++i)
4300+
{
4301+
if (type_removed[i])
4302+
{
4303+
if (!first)
4304+
{
4305+
buf_printf(&opts, ", ");
4306+
}
4307+
/* Send -type to remove all options of that type */
4308+
buf_printf(&opts, "-%s", updatable_options[i]);
4309+
first = false;
4310+
}
4311+
}
4312+
4313+
/* 3. Add new options AND re-add options belonging to removed types */
4314+
const struct push_entry *new_e = new_push_list.head;
4315+
while (new_e)
4316+
{
4317+
if (new_e->enable && new_e->option)
4318+
{
4319+
bool should_send = false;
4320+
bool is_existing = push_option_exists(&old_push_list, new_e->option);
4321+
4322+
/* Check if this option belongs to a type that was reset */
4323+
bool type_was_reset = false;
4324+
ssize_t type_idx = find_updatable_option_index(new_e->option);
4325+
if (type_idx >= 0 && type_removed[type_idx])
4326+
{
4327+
type_was_reset = true;
4328+
}
4329+
4330+
/* Always send new options */
4331+
if (!is_existing)
4332+
{
4333+
should_send = true;
4334+
added++;
4335+
msg(D_PUSH, "MANAGEMENT: Adding: %s", new_e->option);
4336+
}
4337+
/* Also resend options if their type was reset (because we sent -type) */
4338+
else if (type_was_reset)
4339+
{
4340+
should_send = true;
4341+
msg(D_PUSH, "MANAGEMENT: Re-adding (type reset): %s", new_e->option);
4342+
}
4343+
4344+
if (should_send)
4345+
{
4346+
if (!first)
4347+
{
4348+
buf_printf(&opts, ", ");
4349+
}
4350+
buf_printf(&opts, "%s", new_e->option);
4351+
first = false;
4352+
}
4353+
}
4354+
new_e = new_e->next;
4355+
}
4356+
4357+
if (BLEN(&opts) > 0)
4358+
{
4359+
msg(M_INFO, "MANAGEMENT: Updating clients with push options (added=%d, removed=%d)",
4360+
added, removed);
4361+
management_callback_send_push_update_broadcast(m, BSTR(&opts));
4362+
}
4363+
else
4364+
{
4365+
msg(M_INFO, "MANAGEMENT: No changes to send to clients");
4366+
}
4367+
}
4368+
4369+
/* Now replace the old push_list with the new one and free old memory */
4370+
gc_free(&m->top.options.push_list_gc);
4371+
m->top.options.push_list_gc = new_push_list_gc;
4372+
m->top.options.push_list = new_push_list;
4373+
4374+
msg(M_INFO, "MANAGEMENT: Push options reloaded successfully");
4375+
ret = true;
4376+
4377+
cleanup:
4378+
gc_free(&gc);
4379+
return ret;
4380+
}
4381+
41034382
#endif /* ifdef ENABLE_MANAGEMENT */
41044383

41054384

@@ -4125,6 +4404,7 @@ init_management_callback_multi(struct multi_context *m)
41254404
cb.get_peer_info = management_get_peer_info;
41264405
cb.push_update_broadcast = management_callback_send_push_update_broadcast;
41274406
cb.push_update_by_cid = management_callback_send_push_update_by_cid;
4407+
cb.reload_push_options = management_callback_reload_push_options;
41284408
management_set_callback(management, &cb);
41294409
}
41304410
#endif /* ifdef ENABLE_MANAGEMENT */

src/openvpn/options.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ init_options(struct options *o, const bool init_gc)
807807
if (init_gc)
808808
{
809809
gc_init(&o->gc);
810+
gc_init(&o->push_list_gc);
810811
gc_init(&o->dns_options.gc);
811812
o->gc_owned = true;
812813
}
@@ -942,6 +943,7 @@ uninit_options(struct options *o)
942943
if (o->gc_owned)
943944
{
944945
gc_free(&o->gc);
946+
gc_free(&o->push_list_gc);
945947
gc_free(&o->dns_options.gc);
946948
}
947949
}

src/openvpn/options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ struct options
487487
in_addr_t server_bridge_pool_end;
488488

489489
struct push_list push_list;
490+
struct gc_arena push_list_gc;
490491
bool ifconfig_pool_defined;
491492
in_addr_t ifconfig_pool_start;
492493
in_addr_t ifconfig_pool_end;

0 commit comments

Comments
 (0)