Skip to content

LLT-6855: Separation of daemon lifecycle from VPN connection state management - #1909

Open
tomasz-kumor wants to merge 4 commits into
mainfrom
kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite
Open

LLT-6855: Separation of daemon lifecycle from VPN connection state management#1909
tomasz-kumor wants to merge 4 commits into
mainfrom
kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite

Conversation

@tomasz-kumor

@tomasz-kumor tomasz-kumor commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Currently the VPN connection state is tightly coupled with the daemon lifecycle and there is not way to connect/disconnect VPN exit node without changing the NordVPN Lite daemon state.

Solution

  • Separate nordvpnlite daemon lifecycle management from VPN connection state management.
    • Introduced nordvpnlite start --do-not-connect flag to allow separate launching the daemon from establishing VPN connection.
    • Introduced dedicated CLI connect/disconnect commands that allows to control the VPN connection state

Testing

  1. Run daemon without establishing VPN connection
# ./nordvpnlite start --do-not-connect -c config.json

Check that daemon is running but VPN connection is not established

# ./nordvpnlite status
{
  "telio_is_running": true,
  "ip_address": null,
  "exit_node": null
}

Connect to the VPN exit node & check that VPN connection is established

# ./nordvpnlite connect
Command executed successfully
# /nordvpnlite status
{
  "telio_is_running": true,
  "ip_address": null,
  "exit_node": {
    "identifier": "2432424-234536f34f3-4235f34-323",
    "public_key": "cnwurvevke4747tb43y3v74659g45gh45vd5e",
    "hostname": "lt.nordvpn.com",
    "endpoint": "181.88.88.88:51320",
    "state": "connected"
  }
}

Disconnect from the VPN exit node

# ./nordvpnlite disconnect
Command executed successfully
# ./nordvpnlite status
{
  "telio_is_running": true,
  "ip_address": null,
  "exit_node": null
}
  1. Check legacy behavior
# ./nordvpnlite stop
# ./nordvpnlite start -c config.json
...
# /nordvpnlite status
{
  "telio_is_running": true,
  "ip_address": null,
  "exit_node": {
    "identifier": "2432424-234536f34f3-4235f34-323",
    "public_key": "cnwurvevke4747tb43y3v74659g45gh45vd5e",
    "hostname": "lt.nordvpn.com",
    "endpoint": "181.88.88.88:51320",
    "state": "connected"
  }
}

☑️ Definition of Done checklist

  • Commit history is clean (requirements)
  • README.md is updated
  • Functionality is covered by unit or integration tests

@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from f57a61c to e00827b Compare July 24, 2026 09:19
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from e00827b to 2558ede Compare July 24, 2026 11:05
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 958e427 to 0c1644b Compare July 30, 2026 09:52
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 0c1644b to 26f42ca Compare July 30, 2026 15:09
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 26f42ca to 3111f18 Compare July 31, 2026 09:56
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 3111f18 to be8d6ac Compare July 31, 2026 14:23
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from be8d6ac to 9401f81 Compare July 31, 2026 14:25
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 9401f81 to e16efbb Compare July 31, 2026 14:28
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from e16efbb to 59fd4e2 Compare August 3, 2026 10:05
@tomasz-kumor tomasz-kumor changed the title [DRAFT] kumor/llt 6855 only one way of stopping nordvpnlite LLT-6855: start/stop command currently is confusing when used manually - can't stop daemon Aug 3, 2026
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 59fd4e2 to 09d954f Compare August 3, 2026 13:08
@tomasz-kumor
tomasz-kumor marked this pull request as ready for review August 3, 2026 13:21
@tomasz-kumor
tomasz-kumor requested a review from a team as a code owner August 3, 2026 13:21
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 09d954f to 507311f Compare August 5, 2026 11:40
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 507311f to d34ae16 Compare August 6, 2026 09:10
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from d34ae16 to fa5151f Compare August 6, 2026 13:41
tomaszpatejko
tomaszpatejko previously approved these changes Aug 18, 2026
Comment thread Cargo.lock
@tomasz-kumor
tomasz-kumor requested a review from lcruz99 August 21, 2026 07:52
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from f42f791 to 6e90760 Compare August 21, 2026 11:19
pub async fn daemon_event_loop(
mut config: RunningConfig,
logging_handle: &mut logging::LoggingHandle,
auto_connect: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps this param belongs to the config? Not saying to ditch the CLI flag, both can coexist

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not really. It would require a bigger refactoring. Right now the RunningConfig contains the parsed data from the configuration file and some information about the configuration file itself. Command line parameter does not fit to it now.

Comment on lines +395 to +406

if let Err(e) = execute(Command::new("ip").args([
"-6",
"route",
"del",
"default",
"dev",
&self.interface_name,
])) {
error!("Failed to remove IPv6 default route: {e}");
}

@lcruz99 lcruz99 Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seeing this makes me wonder: is there anything else to be cleaned for IPv6? (eg: tables, routes, fwmarks)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was added to make the cleanup implementation symetric to set_exit_routes(). Without that there were sometimes problems with reconnecting after the VPN connection was disconnected.

Comment thread clis/nordvpnlite/src/daemon.rs Outdated
Disconnected,
Connected,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This enum is needless, the exit node status should be checked instead. See examples in the code

@lcruz99 lcruz99 Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isn't what TelioTaskCmd::GetStatus is doing? IIUC, you have created enum VpnConnectionState which is intrinsically Daemon's object, however this is an exclusive property of telio, the protocol lib.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. I have changed the implementation that if exit node was in connected or connecting state after reload, the app will try to re-establish the VPN connection.

Comment thread nat-lab/tests/test_nordvpnlite.py
.await
}
ClientCmd::Connect => {
trace!("Connect");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we can wrap self.telio_task_tx.send() and trace!() on every call. Wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change won't remove too much duplications. I would prefer not try to refactor this code in scope of this PR.

Comment thread clis/nordvpnlite/src/command_listener.rs

#[tokio::test]
async fn test_command_connect_disconnect_error() {
test_command_error_helper(ClientCmd::Connect, "simulated connect failure").await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

'"simulated connect failure"' message doesn't add any info, pls add more details like what was the test intention and what failed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Slightly improved it by adding comments and documenting the helper function.

Comment thread clis/nordvpnlite/src/command_listener.rs Outdated
Comment thread clis/nordvpnlite/src/command_listener.rs

let endpoint_result = ctx
.tokio_handle
.block_on(get_server_endpoints_list(&ctx.config))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This API call can block the whole daemon for long time, and I think would be best to avoid moving the runtime around.

config: NordVpnLiteConfig,
exit_node: Option<ExitNodeStatus>,
connection_state: VpnConnectionState,
/// Handle to the tokio runtime. Used to run async tasks (e.g. HTTP calls) from

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Used to run async tasks

You're using it to run sync tasks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am calling the get_server_endpoints_list() which is async.

exit_node_config: ExitNodeConfig,
) -> Result<(), NordVpnLiteError> {
self.interface_config_provider
.set_exit_routes(&exit_node_config.endpoint.address, &exit_node_config.dns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

set_exit_routes() can be called twice if handle_exit_node_connection() is still fetching the endpoints, this should be fixed by making the endpoints fetch async though.

f"got {status['exit_node']['state']!r}"
)

log.debug("Confirmed: VPN connection intact after duplicate connect")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You're asserting that nordvpnlite reports connected, you should (also) assert that the connection is working independently of what the app says.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How should I do that?

f"but exit_node={status.get('exit_node')}"
)

log.debug("Confirmed: VPN disconnected")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think these tests can be already split into their own categories/classes/mods, e.g.: TestLog, TestConnection.. just like libtelio.

@tomasz-kumor tomasz-kumor Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They could be but there are not that many of them yet. 3 tests for connect/disconnect command and 2 for reload.

@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 6e90760 to 3d885ab Compare August 25, 2026 12:18
@tomasz-kumor

Copy link
Copy Markdown
Contributor Author

Rebased on top of main

Currently, starting the nordvpnlite daemon is strictly coupled with establishing
a VPN connection. This commit introduces a new flag that allows the daemon
startup to be separated from establishing a VPN connection, without breaking
the legacy behaviour.
Without explicitly deleting the IPv6 default route, the route
persisted after the interface was brought down, causing stale
routing entries and potential IPv6 traffic leaks through the
old interface.
After reload, the daemon re-evaluated --do-not-connect instead
of the VPN last state and as a result after the reload VPN connection
state could be different than the pre-reload state.
Currently, establishing a VPN connection is tightly coupled with
the NordVPN Lite daemon start/stop lifecycle.
This commit decouples VPN connection management from daemon state management,
establishing clear separation of concerns between the two functionalities.
@tomasz-kumor
tomasz-kumor force-pushed the kumor/LLT-6855_only_one_way_of_stopping_nordvpnlite branch from 3d885ab to 2549ddd Compare August 25, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants