Whether a hook script can reach the far side of the tunnel depends on three conditions: which event it is attached to, whether OpenVPN is still inside its startup sequence when the script runs, and whether the work has been detached from that sequence. Most scripts fail the third. Adding a sleep treats a lifecycle problem as a timing problem, and it works just often enough to mislead.

Decide the event first, then the mechanism for deferring work, then the place the deferred work runs. Those three decisions are independent, and mixing them up is how a two-line hook becomes an afternoon.

Why the Hook Waits on a Tunnel That Waits on It

OpenVPN calls its hooks synchronously, inside the same process that moves packets. up fires once the tun or tap device exists but before any route is added, and route-up fires after routes; neither moment promises that a peer answers. While a script from either hook is still running, the daemon is not forwarding, so a command that expects traffic to cross the tunnel waits for a tunnel that is waiting for the command. That circular wait is why a manual run passes and the hook times out.

The obvious patch, sleeping a few seconds, sometimes works because the far end came up on its own, not because the sleep fixed anything. A helper that polls for readiness is a different answer: the hook returns, forwarding starts, and the helper gets what it needs once it is actually there.

Pick the Hook by Event, Not by Convenience

Attach up to work that only needs the device, route-up to work that needs routes, and reserve client-connect and client-disconnect for per-client tasks on the server side. Cleanup on disconnect must tolerate being run twice, since reconnects over a persistent interface do not follow the same sequence as a full restart. On the server, client-connect fires when a client authenticates rather than when the daemon starts, which is worth confirming before moving logic there. If the work belongs to the service's lifecycle instead of a single connection, a systemd unit is a better home than any hook.

Hook commands are executed directly by the daemon, with no shell in between, so ifconfig_remote in an up line arrives as literal text. Wrap the command in an explicit shell, or keep the logic in a script that reads the environment. ipchange is one directive that adds arguments of its own: it hands the address and port to the command, so a script written for none sees two unexpected ones.

Three Ways to Detach the Work

The first option is to background the helper from inside the hook: start it with setsid or systemd-run --scope, close the inherited descriptors, and exit zero at once. The second is to have the hook write a small state file and let a systemd timer or a path unit act on it, which keeps the hook trivial and puts the retry policy somewhere that has logs. The third is to drop the hook entirely and move the work into a unit ordered after the VPN service, which is the right answer when the work is not per-connection at all. All three share one rule: the hook has to return before it needs the tunnel.

Whichever you choose, make the helper idempotent and observable. Two overlapping invocations should not run the same irreversible action twice, and a helper that fails has to leave something a human can read.

Separate Sandbox Failures from Lifecycle Failures

When a script runs from a root prompt and fails as a service, the cause is usually a sandbox rather than the network. A restrictive ProtectHome, a read-only ProtectSystem, or a working directory that does not exist can leave the script or its data unreachable, and on an SELinux host the file also needs a context that permits execution. Read the unit with systemctl cat openvpn-server@server and the journal with journalctl -u openvpn-server@server -n 80, then look for a permission or context error rather than a connection timeout. Disabling the sandbox should be the last move, not the first.

A lifecycle failure looks different: the hook starts, the helper runs, and the socket is simply not there yet. Write a timestamped line from the hook itself, disconnect and reconnect, and read the events in order. When the helper truly needs the tunnel, let it poll for readiness rather than sleep for a fixed period.

Three fields make the next report useful: which hook ran, which account it used, and where it stopped. Without them, every later investigation starts over.

Ebitabo ebifaananyi