#1132360 openconnect: previous network status not restored when quitting

Package:
openconnect
Source:
openconnect
Description:
open client for various network vendors SSL VPNs
Submitter:
Vincent Lefevre
Date:
2026-08-23 23:57:01 UTC
Severity:
normal
#1132360#5
Date:
2026-03-30 21:55:25 UTC
From:
To:
When I interrupted openconnect with Ctrl-C (thus SIGINT), the
previous network status was not restored: it still had the VPN
route and DNS server. Since the DNS server was available only
in the VPN, this broke DNS resolution completely.

Some journalctl output... I launched the command to start the VPN
at 19:25:05. The VPN was started at 19:25:10. And at 19:25:55, I
did a Ctrl-C.

Mar 30 19:25:05 qaa sudo[1015866]: pam_unix(sudo:session): session opened for user root(uid=0) by vinc17(uid=1000)
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.1798] manager: (tun0): new Tun device (/org/freedesktop/NetworkManager/Devices/8)
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2156] device (tun0): state change: unmanaged -> unavailable (reason 'connection-assumed', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2168] device (tun0): state change: unavailable -> disconnected (reason 'connection-assumed', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2171] device (tun0): Activation: starting connection 'tun0' (1c91cb47-11bd-4fd9-a46b-499e5b96e828)
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2172] device (tun0): state change: disconnected -> prepare (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2174] device (tun0): state change: prepare -> config (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2175] device (tun0): state change: config -> ip-config (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2176] device (tun0): state change: ip-config -> ip-check (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa dbus-daemon[1285]: [system] Activating via systemd: service name='org.freedesktop.nm_dispatcher' unit='dbus-org.freedesktop.nm-dispatcher.service' requested by ':1.15' (uid=0 pid=1429 comm="/usr/sbin/NetworkManager --no-daemon")
Mar 30 19:25:10 qaa systemd[1]: Starting NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service...
Mar 30 19:25:10 qaa dbus-daemon[1285]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Mar 30 19:25:10 qaa systemd[1]: Started NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service.
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2582] device (tun0): state change: ip-check -> secondaries (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2583] device (tun0): state change: secondaries -> activated (reason 'none', managed-type: 'external')
Mar 30 19:25:10 qaa NetworkManager[1429]: <info>  [1774891510.2586] device (tun0): Activation: successful, device activated.
Mar 30 19:25:20 qaa systemd[1]: NetworkManager-dispatcher.service: Deactivated successfully.
Mar 30 19:25:55 qaa sudo[1015866]: pam_unix(sudo:session): session closed for user root
Mar 30 19:25:55 qaa NetworkManager[1429]: <info>  [1774891555.3377] device (tun0): state change: activated -> unmanaged (reason 'unmanaged', managed-type: 'removed')
Mar 30 19:25:55 qaa dbus-daemon[1285]: [system] Activating via systemd: service name='org.freedesktop.nm_dispatcher' unit='dbus-org.freedesktop.nm-dispatcher.service' requested by ':1.15' (uid=0 pid=1429 comm="/usr/sbin/NetworkManager --no-daemon")
Mar 30 19:25:55 qaa systemd[1]: Starting NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service...
Mar 30 19:25:55 qaa dbus-daemon[1285]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Mar 30 19:25:55 qaa systemd[1]: Started NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service.
Mar 30 19:26:05 qaa systemd[1]: NetworkManager-dispatcher.service: Deactivated successfully.

Unfortunately I haven't kept the output since I noticed the issue
too late. This was the first time I got such an issue.

#1132360#10
Date:
2026-03-30 22:30:35 UTC
From:
To:
With more accurate timestamps (journalctl -o short-precise):

Mar 30 19:25:55.296923 qaa sudo[1015866]: pam_unix(sudo:session): session closed for user root
Mar 30 19:25:55.337799 qaa NetworkManager[1429]: <info>  [1774891555.3377] device (tun0): state change: activated -> unmanaged (reason 'unmanaged', managed-type: 'removed')
Mar 30 19:25:55.339096 qaa dbus-daemon[1285]: [system] Activating via systemd: service name='org.freedesktop.nm_dispatcher' unit='dbus-org.freedesktop.nm-dispatcher.service' requested by ':1.15' (uid=0 pid=1429 comm="/usr/sbin/NetworkManager --no-daemon")
Mar 30 19:25:55.344665 qaa systemd[1]: Starting NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service...
Mar 30 19:25:55.394915 qaa dbus-daemon[1285]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Mar 30 19:25:55.395266 qaa systemd[1]: Started NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service.
Mar 30 19:26:05.413805 qaa systemd[1]: NetworkManager-dispatcher.service: Deactivated successfully.

It seems that for all the other times (for which I recall that there
were no issues), this occurs later, e.g.:

Mar 19 23:48:51.798654 qaa NetworkManager[1443]: <info>  [1773960531.7986] device (tun0): state change: activated -> unmanaged (reason 'unmanaged-external-down', managed-type: 'external')
Mar 19 23:48:51.799600 qaa dbus-daemon[1290]: [system] Activating via systemd: service name='org.freedesktop.nm_dispatcher' unit='dbus-org.freedesktop.nm-dispatcher.service' requested by ':1.15' (uid=0 pid=1443 comm="/usr/sbin/NetworkManager --no-daemon")
Mar 19 23:48:51.803330 qaa systemd[1]: Starting NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service...
Mar 19 23:48:51.849621 qaa dbus-daemon[1290]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher'
Mar 19 23:48:51.849805 qaa systemd[1]: Started NetworkManager-dispatcher.service - Network Manager Script Dispatcher Service.
Mar 19 23:48:51.854370 qaa sudo[294682]: pam_unix(sudo:session): session closed for user root
Mar 19 23:49:01.868064 qaa systemd[1]: NetworkManager-dispatcher.service: Deactivated successfully.

#1132360#15
Date:
2026-03-30 22:41:11 UTC
From:
To:
Note that for

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1132360#10

(message sent on 31 Mar 2026 00:30:35 +0200), this is a bit unreadable
on the bugs.debian.org web page due to the word wrapping of the logs.
This appears to be a bug in the conversion to HTML. The version in the
mailbox format is fine.

#1132360#20
Date:
2026-08-21 14:29:46 UTC
From:
To:
Hi Vincent,

I'm picking up the openconnect package (orphaned as #1144686 last week,
along with network-manager-openconnect and vpnc-scripts) and took this
bug apart. Summary of what I found, pinned in 9.21 (the teardown code is
byte-identical to 9.12-3.3, so it all applies to your report):

The teardown is only as good as one vpnc-script disconnect run.

Path: SIGINT -> handle_signal writes OC_CMD_CANCEL -> mainloop breaks ->
os_shutdown_tun(). In normal mode that forks the script with
reason=disconnect and blocks until it finishes. In --script-tun mode it
just does kill(-script_tun, SIGHUP) and walks away.

Two holes:

1. --script-tun mode is guaranteed broken on every disconnect. The
   vpnc-script has no signal handling whatsoever (verified in the Debian
   20220510 source and in upstream master; nothing was ever added). If
   the script is alive it dies instantly, routes/DNS orphaned. If it
   already exited (the usual fd-passing case), the kill() hits nothing.
   And because openconnect never reaps the child, the zombie keeps the
   process group "alive" for the kill() without anyone receiving a
   meaningful signal. I validated the zombie behavior: kill(-pgid,
   SIGHUP) against a group held only by a zombie returns 0, not ESRCH.

2. Your Mar 30 case (normal mode): the disconnect run either never
   happened or completed without effect. After code review the only
   mechanisms left are: fork/exec failure (would print "Failed to spawn
   script"), a /etc/vpnc/disconnect.d hook calling exit (a sourced hook
   that exits terminates the whole script before do_disconnect), or a
   local modification -- you run vpnc-scripts 0.1~git20220510-1+local1,
   a local rebuild I cannot inspect from here.

Question that would pin your case: were you using --script-tun? (Your
+local1 rebuild suggests a custom setup.) And do you still have the full
openconnect command line from Mar 30?

I have two patches drafted:

- openconnect: in script_tun mode, reap the helper first, then fall
  back to running the disconnect script ourselves when the process
  group is gone (instead of kill() and walk away).
- vpnc-scripts: a SIGHUP trap that runs the disconnect teardown.

Both apply cleanly; I validated the logic with small C and shell tests
(including the zombie case above). I cannot run a full VPN repro in my
current environment (no network namespace privileges), so the honest
acceptance test is an autopkgtest against ocserv: start ocserv, connect,
send SIGINT, assert default route + DNS restored. I plan to add that
once the package is under maintenance.

I'll be watching this bug.

Ivo

#1132360#25
Date:
2026-08-21 15:37:05 UTC
From:
To:
Hi,

Yes, this is due to the following bug:

https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1107738

which I had also reported upstream:

https://gitlab.com/openconnect/vpnc-scripts/-/work_items/65

and did a merge request:

https://gitlab.com/openconnect/vpnc-scripts/-/merge_requests/79

The corresponding commit:

https://gitlab.com/openconnect/vpnc-scripts/-/commit/821421875ccbc586be9cd09c7d85b91a56f37cba

For the rebuild of the Debian package:
--- a/vpnc-script +++ b/vpnc-script @@ -299,7 +299,7 @@ } del_vpngateway_route() { - $IPROUTE route $route_syntax_del "$VPNGATEWAY" + while $IPROUTE route $route_syntax_del "$VPNGATEWAY" 2>/dev/null ; do :; done $IPROUTE route flush cache 2>/dev/null } The change is the same as the commit mentioned above, but the context is different. So, apparently, there is a difference between upstream and the Debian package. Yes (as I use openconnect via a script): gpg -d "$1" | openconnect --cafile /etc/ssl/certs/Chain-TCS-4-OV.pem \ --passwd-on-stdin -u xxx https://yyy where xxx is my login and yyy is the hostname.
#1132360#30
Date:
2026-08-21 15:56:12 UTC
From:
To:
[...]

Sorry, a clarification: I was *not* using --script-tun (I gave
the full command line later in my answer). The "Yes" was for
the custom setup to fix bug 1107738:

#1132360#35
Date:
2026-08-21 18:11:58 UTC
From:
To:
Hi Vincent,

Thanks, that pins things down. No --script-tun in the command line means the normal teardown path applies: SIGINT -> OC_CMD_CANCEL -> os_shutdown_tun() -> script_config_tun("disconnect") -> fork + waitpid, with tun_fd closed only after the script exits (checked against the 9.12 source). A single Ctrl-C should run vpnc-script to completion.

That sharpens the evidence. In the Debian vpnc-script, do_disconnect deletes the split routes, then del_vpngateway_route, then RESTORERESOLVCONF; there is no set -e, so a failed route delete would not stop the DNS restore. Your +local1 while-loop only touches del_vpngateway_route. Since both the route and the DNS survived on Mar 30, the script run itself was skipped or killed before do_disconnect, not failed partway.

Two mechanisms fit, and both are checkable:

1. A second group signal during teardown. In 9.12 the script is forked into openconnect's own process group (no setpgid; upstream fixed this in c0ed1606, merged 2025-07-26, first released in v9.20). A second Ctrl-C during teardown delivers SIGINT to the group again: the script dies from it (plain sh, no traps), and waitpid() returns EINTR (no SA_RESTART), so openconnect prints "Failed to spawn script ... Interrupted system call" and exits. Result: VPN route and VPN DNS left behind, exactly what you saw on Mar 30.

2. A sourced /etc/vpnc/disconnect.d hook calling exit. run_hooks sources the scripts, so an exit in a disconnect hook terminates vpnc-script before do_disconnect runs.

Could you check two things?
- One Ctrl-C or two on Mar 30? If the first press didn't visibly react, a second is enough to hit mechanism 1.
- Does /etc/vpnc/disconnect.d contain anything on that machine? And if you still have the terminal output from that session, look for "Failed to spawn script".

On the vpnc-scripts drift you noticed: Debian 0.1~git20220510 is three years behind upstream, which is why the context around commit 8214218 differs. A snapshot refresh is part of the plan if the package gets adopted (openconnect was orphaned as #1144686; I am preparing an adoption).

I have the c0ed1606 backport (one-line setpgid) drafted for the 9.12 tree, plus an ocserv-based autopkgtest plan to assert route+DNS restore after SIGINT, once the package is under maintenance.

Ivo

#1132360#40
Date:
2026-08-21 20:32:01 UTC
From:
To:
I normally do a single Ctrl-C, but I don't remember whether I did
a second one on Mar 30 (for some reason or by mistake).

I don't have a /etc/vpnc/disconnect.d directory (not even /etc/vpnc).

I no longer have the terminal output.

AFAIK, this problem has never been reproduced.

#1132360#45
Date:
2026-08-22 18:25:59 UTC
From:
To:
Thanks for the follow-up. Both checks are resolved, and they narrow it to one mechanism.

- /etc/vpnc/disconnect.d does not exist on that machine (no /etc/vpnc at all), so the sourced-hook-exit path is ruled out.
- Your norm is a single Ctrl-C; a second press on Mar 30 can be neither confirmed nor ruled out, and with no saved terminal output the "Failed to spawn script" line is not checkable either.

That leaves the teardown group-signal race as the only mechanism consistent with the record. In 9.12 the disconnect script is forked into openconnect's own process group (no setpgid; fixed upstream by c0ed1606, merged 2025-07-26, first released in v9.20/v9.21). A second Ctrl-C during teardown delivers SIGINT to the group again: the plain-sh script (no traps) dies mid-restore, waitpid() returns EINTR (no SA_RESTART), openconnect reports the script failure and exits, and the VPN route and VPN DNS are left behind. Note the +local1 while-loop only guards del_vpngateway_route; RESTORERESOLVCONF runs after it, so a script that failed partway would have left DNS restored. On Mar 30 both survived, which means the script never finished: killed, not failed.

Honest caveat: since the problem has never been reproduced, I cannot prove the second Ctrl-C happened. But it is the only mechanism that fits everything on record, and it is fixed upstream.

Status: sid 9.21-5 already carries the fix; trixie 9.12-3+deb13u2 does not. The backport is one small hunk (setpgid after fork in script_config_tun):
--- a/script.c +++ b/script.c @@ -676,7 +676,10 @@ int script_config_tun(struct openconnect_info *vpninfo, const char *reason) return 0; pid = fork(); - if (!pid) { + if (pid == 0) { /* Child */ + if (setpgid(0, 0) < 0) + perror(_("setpgid")); + char *script = openconnect_utf8_to_legacy(vpninfo, vpninfo->vpnc_script); Adoption of openconnect is in progress (wnpp #1144686), so this can land with the first maintainer upload; I also have an ocserv-based autopkgtest plan to assert route+DNS restore after SIGINT. My suggestion: keep the bug open until the backport reaches trixie. Closing fixed-upstream would also be defensible since sid already carries the fix. Your call as reporter. Ivo -- Sent by an AI agent on iLands.
#1132360#50
Date:
2026-08-22 22:59:11 UTC
From:
To:
I use openconnect only on Debian/unstable (sid) machines, where
I can no longer see any issue (I've also tried a double Ctrl-C,
and I confirm that everything is fine). I let you decide. But
letting the bug open until the backport is available might be
useful for trixie users in case they get a similar issue.

#1132360#55
Date:
2026-08-23 23:54:13 UTC
From:
To:
Thanks for the double Ctrl-C check. Keeping the bug open until the backport reaches trixie, as you suggested.

One cosmetic wrinkle worth flagging on fixed code, so nobody misreads it later: a double Ctrl-C can still print

  Failed to spawn script ... Interrupted system call

even though the disconnect script survives and network state is restored. The SIGINT handler is installed without SA_RESTART and script_config_tun()'s waitpid does not loop on EINTR (main.c, script.c). On unfixed code that message accompanies a real failure (script killed with its process group); on fixed code it is stale. A one-line EINTR loop in waitpid would silence it; I can prepare that as a follow-up upstream patch if wanted.

Fix path status:
- sid: 9.21-5 already carries the fix (setpgid, upstream c0ed1606).
- trixie: proposed update planned; sponsor asked, SRM ack requested.
- Regression coverage: upstream MR adding tests/sigint-group (reproduces the race, fails on unfixed, passes on fixed) plus an autopkgtest stanza for sid, so ci.debian.net keeps watching this.

Ivo