Your home-network VPN has nothing to do when the laptop is already sitting on that network. The tempting shortcut is a preparation command that exits nonzero, which fails the unit and makes every dashboard look broken. systemd has a directive for exactly this shape of decision, and it separates a deliberate skip from a real fault. Getting it right is mostly about writing down what each exit code means.

Why a failing precondition is the wrong shape

ExecStartPre treats a nonzero exit as a startup failure, which is correct for a command that prepares the service. Using it as a conditional makes a policy decision indistinguishable from a broken dependency. ExecCondition exists so a unit can decline to start without being marked failed.

That distinction matters at three in the morning, when nobody wants to triage a red unit that was behaving as designed. A failed unit also hides the real failures among the intentional ones.

Write the exit-code contract first

Write the contract before the script. Exit zero means continue, exit one means skip the remaining start commands, and exit 255 means the condition itself could not run. Codes between two and 254 normally mean skip as well, which is easy to forget when a helper returns a random error code. The drop-in below attaches the helper to the real unit name.

[Service]
ExecCondition=/usr/local/libexec/should-start-home-vpn

Check the effective unit for inherited SuccessExitStatus or stop-post settings before assuming default semantics. Then test each of the three outcomes deliberately.

A network hint is not proof of location

A reachable router address or a known MAC can suggest the right network, and neither proves it. Another network can reuse the same address, ARP data can be stale, and identifiers can be imitated. Treat the probe as an optimisation that is consistent with policy, not as an authentication result.

Decide what uncertainty means in advance. Starting the VPN when unsure is safe for a client that would simply fail to connect; skipping it when unsure is not. Make the probe bounded, and read the active interface rather than assuming the first one listed. A probe that hangs is worse than one that answers no, because the unit waits for it.

Record those assumptions next to the unit file so the next person knows what the helper believes. A comment at the top of the script is the cheapest place to keep them.

Handle the move between networks

The condition runs when the service starts, and it does not re-evaluate later. A skipped unit stays skipped after the laptop moves to a coffee shop, and a running tunnel stays up after the laptop comes home. Connect the network-change lifecycle to the start and stop decision with a separate mechanism.

systemctl cat home-vpn.service

Test four cases: startup at home, startup away, a probe that cannot run, and a move between networks while the unit is active. Keep the skip reason visible in the journal so an intentional decision never looks like an authentication failure. The service reference documents the exit semantics and the minimum version.

Skyldar greinar