Wiring a dependent service to tun0.device looks like subscribing to the tunnel, and it is not. A device unit reflects that the interface exists, which can stay true across a failed connection and the reconnect that follows. The service then acts at the wrong moment, or never, depending on whether the interface disappeared in between. The hook that describes the event you actually care about is the one OpenVPN calls, not the one systemd infers.

Decide which condition the service needs before choosing the trigger, because the three candidates are not interchangeable.

What the device unit actually tracks

systemd creates a device unit from the kernel's view of a network interface, so tun0.device becomes active when the interface appears and goes inactive when it is removed. With persist-tun, a soft restart keeps the interface in place, so the unit never flaps and nothing bound to it is triggered. Even without persistence, the unit says nothing about routes, DNS, or whether the far end answers.

That makes it a poor proxy for 'the VPN is ready'. A listener that only needs the local address might be satisfied by interface existence, while a service that calls an application on the remote network needs routes and reachability, which the device unit cannot express.

up, route-up and up-restart mean different things

up runs after the TUN/TAP device is opened and the interface is configured, which is the right moment for a listener that binds the assigned address. route-up runs after routes are installed, subject to any configured route delay, and suits work that needs the route stage complete. Neither one proves that the remote application answers. Choosing between them is a question about what the service needs, not about which hook sounds more finished.

With persist-tun, restarts reuse the device, so up does not run again unless up-restart is set. That option enables the up script for the documented restart lifecycle, and its environment is reduced compared with a first start. A script that assumes the full environment works on the first connection and fails on the next one.

Dispatch the action without waiting on it

A hook that calls systemctl restart synchronously can deadlock against a unit whose ordering waits for the tunnel. systemctl --no-block try-restart vpn-dependent.service queues the job and returns, which keeps the hook from blocking the connection it was reacting to. try-restart acts only on a running unit, while restart also starts an inactive one; pick deliberately rather than by habit.

Queuing is not verification. The hook returns success as soon as the job is accepted, so the check has to happen in the target unit's journal or in its actual listener state.

Restart policy lives in the unit, not the hook

A oneshot unit with RemainAfterExit=yes stays active after its command exits, and a second start request does nothing because systemd considers it already started. That is a property to design around, not a defect to work around with repeated start calls. If the action must run per connection, the unit needs a shape that permits it.

After= orders units, and BindsTo= ties lifetimes, but neither means 'restart this whenever the VPN reconnects'. A unit bound to the tunnel stops when the tunnel stops; it does not restart itself when the tunnel returns unless something starts it again.

Test three reconnects, not one

Cold boot, a full client restart, and a soft reconnect each exercise a different path. The soft reconnect is the one that exposes a missing up-restart, a reduced environment, or a device unit that never went inactive. Run all three and confirm both directions: the service acts when the tunnel comes up, and it does not restart on unrelated link changes such as a Wi-Fi roam.

Keep an independent recovery path when the dependent service carries your own administration access. A hook that restarts the thing you are connected through is a change that can lock you out.

འབྲེལ་བའི་རྩོམ་ཡིག