At boot, a socket unit that binds a fixed VPN address can fail before the tunnel has assigned it, and the service behind it never starts. Two switches look like fixes and solve different halves of that problem. FreeBind lets systemd create the listener while the address is still absent. After= only orders jobs, so it changes when things start without making the address arrive any sooner.

Two different problems, two different switches

The first problem is a binding failure: the kernel refuses a listen on an address the host does not own, so the socket unit fails immediately and the service is never activated. The second problem is a readiness failure: the address does exist eventually, but something tries to use it before the tunnel carries traffic. FreeBind addresses the first and does nothing for the second. Ordering directives address neither directly, because they only decide what starts in which sequence. Naming the failure you actually have is the entire decision.

Confirm which one you have from the journal rather than from the service description. A binding failure names an address and reports that it cannot be assigned. A readiness failure usually appears later, as a timeout or a refused connection seen from the far side, with a perfectly healthy socket unit in the logs.

FreeBind: the listener exists before the address

FreeBind tells the kernel to allow a bind to a non-local address, so systemd can create the listening socket at boot and hand it to the application whenever the application starts. That is the point of socket activation, and the reason the socket can outlive the service behind it.

[Socket]
ListenStream=
ListenStream=10.8.0.1:8443
FreeBind=yes

The empty assignment clears the listen entries inherited from the vendor unit before the new one is added, which prevents a leftover wildcard listener from answering on every interface. Replace the address and port with your own, and verify the effective unit rather than the drop-in file alone. Use a drop-in under the unit name instead of editing a package file in place, because the next package update will overwrite the edit and leave no trace of the intent. Then apply it through the reload and restart procedure that the socket unit expects.

The systemd socket reference documents the IPv4 and IPv6 behaviour behind the option, and the important detail is that it applies to the bind rather than to routing. Nothing in it assigns an address or starts a VPN. FreeBind is a permission granted to the listener, not a replacement for the interface.

After= and Wants=: ordering is not readiness

Ordering and requirement are separate axes, and confusing them produces units that start in the right order and still fail. After=vpn-client.service means only that if both jobs are started, this one waits for the other to be started. It does not pull the VPN in, and it does not wait for the VPN to finish connecting.

Requirement directives such as Wants= and Requires= handle activation, and they differ in what happens when the dependency fails: one continues, the other stops. Neither of them knows what a connected tunnel means. A VPN process can be active with no address assigned, or with an address assigned and no route to the peer, and both states satisfy the dependency. If the application needs the address to exist, model that condition explicitly instead of treating process startup as proof of readiness.

Check what the unit actually declares with systemctl cat and systemctl show -p After -p Wants -p Requires. Drop-ins from three different directories can add directives you never wrote, and the effective unit is the only thing systemd reads.

What network-online.target does not do

A unit ordered after network-online.target waits for the network manager to declare the network usable, which is a boot synchronization point rather than a continuous condition. It says nothing about whether a specific tunnel is up, and it will not fire again when a VPN reconnects in the middle of the afternoon. Treating it as a reconnect monitor is the most common misreading of that target.

The systemd project documents that distinction directly, and the practical consequence is that anything needing a tunnel address has to own the dependency itself. A unit that waits for network-online and then binds a tunnel address is correct only when the tunnel happens to come up first, which is a coincidence rather than a design.

Test the lifecycle you actually chose

Test a clean boot, a request after the address appears, and the behaviour after the VPN drops and returns. Those three cases separate a working listener from a working service.

systemd-analyze verify myapp.socket
systemctl cat myapp.socket
ss -ltn "sport = :8443"

The first command catches unit-file errors before a reboot, the second shows the merged unit, and the third proves whether the listener exists and on which address. A listener on a wildcard address where you expected a tunnel address is exactly the leftover that the empty ListenStream was meant to remove. Repeat the third command after a VPN reconnect to see whether the address moved.

Choose the lifecycle deliberately: either a free-bound listener that waits for a stable address to return, or a service that stops with the tunnel and starts again on recovery. Those are different designs and they fail in different ways. Then keep access policy tied to the sources and paths you intended, because binding a destination address does not require that accepted packets arrived through a trusted interface.

Qillqakuna t'inkisqa