The tunnel is down but the machine still behaves as if it is up: a lookup hangs until it times out, and a route points into an interface that no longer exists. That is the signature of an openconnect process that died without running its disconnect helper. The network state it created is still installed, and nothing else on the system knows how to remove it.

Walk the machine in the order the state was created: process, link, route, resolver. Each step tells you whether the client is really gone or just detached from your terminal, and that distinction decides whether you repair anything at all.

Start with the process, not the configuration

Open a terminal and ask what is still running before touching a route. pgrep -a openconnect lists every instance with its arguments, which shows the pid file and the script it was given. A process you did not expect explains the phantom tunnel faster than any log.

If nothing is running, look at the link. ip link show tun0 tells you whether the interface survived, and ip route show dev tun0 lists routes still bound to it. Then check the resolver with resolvectl status. Record all three outputs before changing them, because that record is how you tell a stale entry from a new one later.

An interface that is gone normally takes its routes with it. Routes that outlive the link usually mean the process is still alive or a second client owns the device.

The helper script is what undoes the changes

openconnect does not remove the interface, routes or resolver entries itself. It calls an external helper, /etc/vpnc/vpnc-script in an upstream build and /usr/share/vpnc-scripts/vpnc-script in the Debian and Ubuntu packages, chosen with -s. The same script that sets the tunnel up is the one that tears it down.

If the process exits without calling that helper, the state stays. The OpenConnect manual is explicit that routing and name service will not work correctly without the script, and it describes the helper as the component that dismantles the interface state on exit. That is why the path matters when you launch the client with -s.

Which signal reaches that path changed between versions. The 7.06 manual treats SIGTERM as an immediate exit that skips the script. Current builds document INT and TERM as a clean stop, where the session is logged off and the helper runs. Read the SIGNALS section of the binary you actually have instead of assuming either behaviour.

man openconnect | sed -n '/^SIGNALS/,/^LIMITATIONS/p'

Send the signal your build documents

Once you know the signal, stop the process by pid rather than by pattern.

pgrep -a openconnect
sudo kill -INT "$(cat /var/run/openconnect.pid)"

Then repeat the three checks from the first section instead of trusting the exit status. ip link show tun0 should fail, ip route show dev tun0 should print nothing, and resolvectl status should no longer list the VPN link.

SIGHUP is not a stop button: it disconnects, runs the script, and leaves the session resumable with --cookie. SIGUSR2 forces an immediate reconnect. SIGKILL skips cleanup entirely, which is exactly the state this article is about.

Repair the resolver without replacing resolv.conf

On most Ubuntu systems /etc/resolv.conf is a symlink into systemd-resolved, not a file you should edit. Replacing it breaks name resolution for every network, not just the VPN. Drop the tunnel's registration instead:

resolvectl revert tun0
resolvectl query example.com

If the system uses resolvconf rather than systemd-resolved, the per-interface record persists until it is told to drop the link, and resolvconf -d tun0 is the equivalent action. Check which resolver is in use before choosing the command.

A lookup still aimed at a server behind the dead link hangs until it times out, and that delay looks like a broken DNS server rather than a leftover route.

Make the next disconnect boring

Keep the pid file path identical at startup and shutdown by passing --pid-file every time, or run openconnect under a unit whose KillSignal matches the signal your version documents. Testing in the foreground first shows you the shutdown messages the helper prints.

Connect, disconnect, and run the three checks once. A clean exit leaves no VPN link, no VPN route and no VPN resolver entry. If one of the three survives, you now know which component failed to clean up. The next investigation then starts from that answer instead of from a hanging lookup.

Pennadoù liammet