Skip to content

Conversation

@maurice2k
Copy link

Overview

This PR introduces a new management command reload-push-options to allow dynamic updating of client configurations (routes, DNS, etc.) without restarting the server or disconnecting clients. It also includes a critical fix for PUSH_UPDATE handling when messages are split across multiple chunks (continuation messages).

1. Feature: reload-push-options

Administrators can now trigger a reload of push options from the server configuration file via the management interface.

Usage

reload-push-options [sync]
  • Without arguments: Reloads push options from the config file. New clients connecting after this will receive the new options. Existing clients are unaffected.
  • With sync: Reloads options AND synchronizes them to currently connected clients.

Sync Logic

When sync is used, the server:

  1. Parses the new configuration.
  2. Calculates the difference between the old and new push lists.
  3. Sends removal commands (e.g., push-update-broad "-route") for option types that have changed or been removed.
  4. Sends the new options via PUSH_UPDATE.

This allows for seamless updates of routing tables and network settings on live VPNs.

2. Bug Fix: PUSH_UPDATE Continuation Logic

The Issue

Previously, the state tracking for which option types had been "reset" (e.g., OPT_P_U_ROUTE) was stored in a local variable update_options_found within apply_push_options().

If a PUSH_UPDATE message was large enough to be split into multiple fragments (using push-continuation), this state was lost between fragments.

  • Result: The route table (or other list options) would be reset for every fragment that contained a route, rather than once for the entire update sequence.
  • Symptom: Large route lists would fail to apply correctly, often resulting in only the last chunk's routes being present, or significant churn/flickering of routes during the update.

The Fix

The state variable has been moved to struct options as push_update_options_found. This ensures the "reset state" persists across the entire PUSH_UPDATE sequence and is only cleared once the full sequence is processed.

Testing

Integration Tests

Added a full docker-based integration test suite in tests/reload_push_options/.

  • Coverage:
    • Basic reload (no sync).
    • Sync adding routes.
    • Sync removing routes.
    • Sync replacing all routes.
    • Stress Test: Verifies handling of 500+ routes (which triggers the continuation bug logic).
  • How to run: tests/reload_push_options/run.sh

Unit Tests

  • Added test_incoming_push_continuation_route_accumulation to test_push_update_msg.c to verify that route lists accumulate correctly across multiple message chunks.

Moritz Fain added 2 commits November 26, 2025 19:10
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 'sync' 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.
Previously, the logic for resetting push options (like 'route') was based on
`update_options_found` which was local to `apply_push_options`. This meant
that if a PUSH_UPDATE was split across multiple continuation messages,
the state was lost, causing routes to be reset multiple times (once per
message chunk) rather than once per update sequence.

This patch moves the state tracking to `struct options` as
`push_update_options_found`, allowing it to persist across the entire
PUSH_UPDATE sequence.

This fixes an issue where large route lists sent via PUSH_UPDATE would
result in only the last chunk's routes being applied, or previous routes
being continuously deleted and re-added.

Added unit test `test_incoming_push_continuation_route_accumulation` to
verify the fix.
@maurice2k
Copy link
Author

@cron2
Copy link
Contributor

cron2 commented Nov 27, 2025

Thanks for working on this, and thanks for sending all the good stuff to the openvpn-devel list right away.

At this point in time, close to the 2.7 release, the big feature enhancement might be a bit too much for us to digest. The bugfix patch really should go in before 2.7.0 - @mrbff can you review?

@mrbff
Copy link
Contributor

mrbff commented Nov 27, 2025

Yes, I will review asap

@maurice2k
Copy link
Author

It looks bigger than it is; most of it is just end-to-end tests. The code change itself is not that much....

Please have a closer look at the gc_arena stuff; I hope I got that right. I'm basically creating a new options.push_list and then switching the current with the new one.

Copy link
Contributor

@mrbff mrbff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

07625d2 patch looks generally good but i think there are some little changes to make

const msglvl_t msglevel = D_PUSH_ERRORS | M_OPTERR;
unsigned int update_options_found = 0;

/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment could be moved above the update_option() call

}
else
{
update_option(c, options, p, false, file, line_num, 0, msglevel, permission_mask,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually at this point the last parameter of update_option() could be removed as we pass the entire options struct anyway. but is just refactor anyway...

goto error;
}
}
/* Clear push_update_options_found for next PUSH_UPDATE sequence */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would suggest to put this reset inside the else (is not a REPLY so is PUSH_UPDATE) and before the possible goto error;

*/

/* Simulate route handling from update_option() in options.c */
if (is_update && strncmp(&line[i], "route ", 6) == 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_update is always true in the this test, no need to check it, only if (!strncmp(&line[i], "route ", 6)) is fine.

options->push_update_options_found |= OPT_P_U_ROUTE;
}
route_add_count++;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually to make the tests work you should add the push-continuation logic otherwise the testdriver will fail.

        if (!strncmp(&line[i], "route ", 6))
        {
        ...
        }
        /* Simulate add_option() push-continuation logic */
        else if (!strcmp(&line[i], "push-continuation 2"))
        {
            options->push_continuation = 2;
        }
        else if (!strcmp(&line[i], "push-continuation 1"))
        {
            options->push_continuation = 1;
        }

Build using make check to run the tests

* continuation message, causing routes to be reset on each message instead
* of accumulating.
*
* Expected behavior: routes should only be reset ONCE (when -route is received),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually the intended behavior is to reset routes (or an option in general) the first time it is encountered. So even if no -route is present, but just route 10.1.0.0 255.255.0.0 for example, all the already existing routes should be deleted. So the comment should be modified accordingly and the -route option in the first message can actually be removed.


/* Message 1: removal + first batch of routes, continuation 2 (more coming) */
struct buffer buf1 = alloc_buf(512);
const char *msg1 = "PUSH_UPDATE,-route, route 10.1.0.0 255.255.0.0, route 10.2.0.0 255.255.0.0, route 10.3.0.0 255.255.0.0,push-continuation 2";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-route could be removed

assert_int_equal(route_add_count, 9);

/*
* BUG CHECK: Routes should only be "reset" once (for the entire update sequence).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about just "We assure that route option is reset only one time in the first message if a push-continuation is present" or something like that? "Currently it will show route_reset_count == 3, exposing the bug." sounds strange to read once the commit is merged.

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 [sync] : Reload push options from config file for new clients.");
msg(M_CLIENT, " With 'sync': also sync options to connected clients (add new, remove old).");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are adding new commands, we may need to increment MANAGEMENT_VERSION in manage.h so that the client can determine whether the option is supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants