- 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
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.
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.
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.
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
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.
[...] 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:
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
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.
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.
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.
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