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 */
0 commit comments