Skip to content

fix(platform): stop on SIGTERM when already CLOSED, and exit when tunnel setup aborts - #346

Open
mastercoding wants to merge 2 commits into
mp0rta:mainfrom
mastercoding:fix/linux-client-abort-and-sigterm
Open

mastercoding wants to merge 2 commits into
mp0rta:mainfrom
mastercoding:fix/linux-client-abort-and-sigterm

Conversation

@mastercoding

@mastercoding mastercoding commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #345. Two commits, both in the Linux platform layer — the library is untouched, so
there is no ABI or shared-behaviour change and the Android/iOS/API consumers are unaffected.

  1. on_signal() breaks the loop itself instead of relying on a CLOSED transition that
    mqvpn_client_disconnect() does not deliver when the state is already CLOSED
    (mqvpn_client.c:3134). That early return is a documented idempotence guarantee
    (tests/test_api.c:802, :814), so the loop exit belongs to the platform — which is the
    shape svr_on_signal() already uses in the same file. One statement and a comment.

  2. The fail: path marks fatal and exits non-zero — the decision platform_windows.c:232-238
    already made ("local-side problems that reconnecting won't fix"), with the same
    rc = ctx.fatal_error ? 1 : 0; at :703. platform_darwin.c carries both shapes verbatim.

Before / after, kill-switch trigger, 30 aborted starts and 30 controls per run, same binary
and stop procedure within each run: 30 of 30 aborted starts needed SIGKILL -> 30 of 30 exit by
themselves with status 1. The controls are unchanged: still come up, still exit 0 on SIGTERM.

How to verify:

ctest -R test_cli_sigterm_closed          # no root, ~8 s
sudo scripts/ci_e2e/run_setup_abort_exit_test.sh   # netns, ~25 s

The first fails on 6b80ddc0 and on b9227aa9 ("client still running 5s after SIGTERM (state CLOSED) / the
handler ran — the event loop was not broken") and passes here in 500 ms. The second has a
control that comes up and pings through the tunnel, then drives fail: with --tun-name on an
existing device: "client still running 20s after the abort" before, "exited 1 after 1s" here.

What this does not fix: Reconnect still does not cover setup failures. Making it do so is a
library-side change — closing the connection as a failed attempt rather than a user disconnect,
so the gate at mqvpn_client.c:1331 fires — which alters shared behaviour for one platform
caller and puts POSIX at odds with Windows. Say the word and I will send that version instead.

One behaviour change, measured both ways: a setup failure on a later reconnect now ends a
client that had been up for days. What it replaces is a process holding the tunnel's name with no
tunnel, that a supervisor can neither see nor stop.

Summary by CodeRabbit

  • Bug Fixes

    • Tunnel setup failures now stop reconnect attempts and return a non-zero exit status.
    • SIGINT and SIGTERM reliably terminate clients, including clients already in a closed or idle state.
    • Improved error logging for tunnel setup failures.
  • Tests

    • Added coverage for setup-abort exit behavior under sanitizer and network-namespace testing.
    • Added coverage for SIGTERM handling after a client reaches the closed state.

mastercoding and others added 2 commits September 17, 2026 09:16
on_signal() set shutting_down, called mqvpn_client_disconnect() and left the
loop to cb_state_changed's CLOSED branch. mqvpn_client_disconnect() returns
early when the state is already CLOSED or IDLE (mqvpn_client.c:3134), so a
client that reached CLOSED on its own got no transition, cb_state_changed never
ran, and the process went on ticking a dead client: SIGTERM and SIGINT did
nothing and only SIGKILL stopped it. The shipped mqvpn-client@.service sets
Restart=on-failure, which never sees the process exit, and `systemctl stop`
falls back to systemd's default TimeoutStopSec; scripts/ci_e2e/run_test.sh
hangs the same way, in stop_and_check_sanitizer's unbounded `wait`
(sanitizer_check.sh:64-65). Break the loop in the handler, as svr_on_signal()
already does. Darwin's on_signal is the same shape and gets the same line.

The new scenario reaches CLOSED without root, a server or a TUN: --no-reconnect
against a port nothing answers on. It fails on the parent commit — "client
still running 5s after SIGTERM (state CLOSED) / the handler ran — the event
loop was not broken" — and passes here, the process gone 500ms after the
signal. Run in a debian:trixie aarch64 container (glibc 2.41), as root and as
an unprivileged user. That the client sits in the loop at CLOSED there is the
scenario's way in, not a behaviour this asserts is right: run_killswitch_test.sh's
Test 3 expects a --no-reconnect client to exit instead, so the test fails
saying so rather than passing empty if that ever becomes true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cb_tunnel_config_ready()'s fail: path — TUN creation, addressing, MTU, routes,
the kill switch — called mqvpn_client_disconnect() and returned into the event
loop. The library reads that as a user disconnect (mqvpn_client.c:3136 sets
shutting_down), so cb_h3_conn_close's retry branch never fires
(mqvpn_client.c:1331) although Reconnect defaults to true (config.c:1280), and
nothing else ended the loop: no tunnel, no retry, no exit, and the shipped
unit's Restart=on-failure with nothing to act on.

platform_windows.c:232-238 already decided this — "local-side problems that
reconnecting won't fix" — and reads the flag at :703. Same three statements in
the same order, and the same rc line, on Linux and on Darwin.

The one behaviour change: a setup failure on a *later* reconnect now ends a
client that has been up for days, where before it idled on with no tunnel.
Measured both ways, kill switch up and then iptables removed under a forced
reconnect — on 6b80ddc "STILL ALIVE 20 s later ... SIGTERM did NOT stop it;
needed SIGKILL", here "EXITED on its own, status 1". On the reported trigger
(--kill-switch, no iptables binary, two netns, debian:trixie aarch64), 30 of 30
aborted starts exited by themselves with status 1 and none logged "reconnecting
in"; on 6b80ddc the same 30 all needed SIGKILL after SIGTERM.

The new netns suite drives fail: with --tun-name on a device that already
exists in the namespace, so TUNSETIFF returns EINVAL inside the callback after
the handshake. It runs in both e2e jobs; its Test 1 control proves an ordinary
client on the same rig still comes up and stays up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 76d2e07c-f38c-43f3-b1da-4ee7a1d2b9b2

📥 Commits

Reviewing files that changed from the base of the PR and between b9227aa and 82d9f4c.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • CMakeLists.txt
  • scripts/ci_e2e/run_setup_abort_exit_test.sh
  • src/platform/darwin/platform_darwin.c
  • src/platform/linux/platform_linux.c
  • src/platform/posix/platform_internal.h
  • tests/test_cli_sigterm_closed.sh

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Linux and Darwin clients now exit after fatal tunnel setup failures and respond to signals in CLOSED or IDLE states. New tests cover SIGTERM handling, setup-abort exit status, setup-failure logging, and sanitizer results.

Changes

Client shutdown behavior

Layer / File(s) Summary
Fatal tunnel setup exit behavior
src/platform/posix/platform_internal.h, src/platform/linux/platform_linux.c, src/platform/darwin/platform_darwin.c
Platform contexts record fatal tunnel setup failures. Linux and Darwin clients return status 1 after the event loop exits.
Signal handling in closed states
src/platform/linux/platform_linux.c, src/platform/darwin/platform_darwin.c, CMakeLists.txt, tests/test_cli_sigterm_closed.sh
Signal handlers break the event loop after disconnect requests, including CLOSED and IDLE states. The CTest case verifies SIGTERM termination from CLOSED.
Setup abort integration coverage
scripts/ci_e2e/run_setup_abort_exit_test.sh, .github/workflows/ci.yml
The end-to-end test creates a TUN name collision, requires exit status 1, checks setup-failure logging, and rejects sanitizer diagnostics. CI runs the test in sanitizer and netns jobs.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: mp0rta

Merge Risk: ⚪ Minimal · up to 82d9f

The shutdown and fatal-setup paths are covered by the stated implementation and integration checks, with no actionable merge risk identified.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Issue #345 is scoped to the Linux client. The PR also changes src/platform/darwin/platform_darwin.c to add fatal tunnel-setup exits, direct signal loop breaks, and fatal exit logging. The linked iss… Remove the Darwin implementation changes, or link a separate requirement that justifies the Darwin behavior changes. Keep shared declarations only when the retained Linux implementation requires them.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 5 files. (2 skipped: 2… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes both main changes: stopping on SIGTERM when the client is already CLOSED and exiting when tunnel setup aborts.
Linked Issues check ✅ Passed Issue #345 requires two Linux client fixes. In src/platform/linux/platform_linux.c, the tunnel setup fail: path sets fatal_error and shutting_down, then the run function returns status 1 after…
Full details: Out of Scope Changes check

Explanation

Issue #345 is scoped to the Linux client. The PR also changes src/platform/darwin/platform_darwin.c to add fatal tunnel-setup exits, direct signal loop breaks, and fatal exit logging. The linked issue provides no Darwin requirement, and these changes alter non-Linux behavior beyond the Linux defects.

Full details: Docstring Coverage

Explanation

Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 5 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux client: an aborted tunnel setup neither retries nor exits, and SIGTERM cannot stop a client that has reached CLOSED

1 participant