A script that must run after the VPN comes up needs an event from the component that owns the connection. A file dropped into an old network hook directory is not a callback for every VPN that NetworkManager, pppd, or a standalone OpenVPN process might create. Each of those owners reports a different event with a different environment, so the owner has to be identified before the hook is chosen. Otherwise the script is correct and simply never fires.

For a NetworkManager-managed VPN, the dispatcher is the right place, and vpn-up is the right action.

Use vpn-up, not the interface event

The dispatcher distinguishes up from vpn-up. The first reports an activated interface; the second reports an activated VPN connection. Wi-Fi coming up only means the underlying link exists; the tunnel may still be negotiating, or may never authenticate at all.

The script receives the interface and the action, while connection details arrive through the environment, including the profile UUID and the VPN IP interface. Read those variables instead of inferring identity from device names.

Scope the match to one connection

Match on the connection UUID, or another identity the dispatcher supplies, rather than a tun prefix or ppp0. Interface names are not stable, more than one tunnel can exist at the same time, and two profiles can carry the same display name. Matching a tun prefix means the script will eventually fire for a connection nobody meant to touch. It can also fire twice for one profile when a reconnect rebuilds the device.

Make the task idempotent. A callback that appends a route on every invocation eventually fills the table with duplicates, and the second copy is the one that breaks routing.

Know what activation does not prove

vpn-pre-up runs before NetworkManager reports the VPN fully activated, and the manager waits for those scripts. Use a blocking stage only when the task genuinely belongs there, because a slow check delays the activation every other component is waiting to observe.

Routes being present does not prove an application is listening, a private name resolves, or a user session exists. Check those separately with a bounded timeout, and report which layer failed instead of retrying the whole chain.

Respect the context the script runs in

Dispatcher scripts run as root, outside any user session. Do not assume the logged-in user's environment, graphical session, or unlocked keyring is available. Nobody should respond to a missing desktop context by making every credential readable by every process. Keep user-session work in a user service or a session helper that the callback can signal without impersonating the user.

Ownership and permissions on the script matter too. A root-executed file that any user can replace is a privilege escalation path, and it is a finding independent of whether the VPN logic is correct.

Plan for the disconnect that never fires

NetworkManager documents that vpn-pre-down is not emitted for forced disconnections such as an unexpected termination or lost connectivity. So cleanup built only around a graceful pre-down is incomplete by construction. Any shared state the script changes needs another owner that can restore it after a crash, and that recovery path has to be tested rather than assumed.

Exercise the hook with a normal connect, a reconnect, an intentional disconnect, and a pulled cable. Record the UUID, the action, and the interface that fired, without logging secrets. That record is the only reliable evidence that the hook ran for the connection you meant, and it is what makes the next change safe.

Qillqakuna t'inkisqa