Skip to content

Reduce everything to macOS and Wireguard - #4

Merged
pansen merged 51 commits into
mainfrom
chore/andi/wg_solo
Jul 18, 2026
Merged

Reduce everything to macOS and Wireguard#4
pansen merged 51 commits into
mainfrom
chore/andi/wg_solo

Conversation

@pansen

@pansen pansen commented Jun 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

pansen added 30 commits June 14, 2026 09:13
### The deadlock chain

The helper runs on a **`new_current_thread`** Tokio runtime (`src/userspace_helper.rs:212`). Inside `wait_for_shutdown` it ticks once per second and, on macOS, runs a diagnostic probe **synchronously, inline, blocking the executor**:

- `src/userspace_helper.rs:392` — `next_diag_at = Instant::now()`
- `src/userspace_helper.rs:388` — `interval(1s)`; tokio's first `tick()` returns **immediately**
- `src/userspace_helper.rs:428-433` — first tick → `log_macos_dataplane_probe(...)` (a plain blocking call, not awaited)
- `src/userspace_helper.rs:451 → 505-508` — `read_wg_transfer_bytes` runs `wg show <iface> transfer` via `Command::output()` (no timeout), where `<iface>` = `control_interface_name` = `wgconf0`.

`wg show wgconf0 transfer` connects to `/var/run/wireguard/wgconf0.sock` — **gotatun's own in-process UAPI server**. Servicing that request requires gotatun's async `handle_api` task to run:

- gotatun's UAPI accept/IO runs on a dedicated `std::thread` (`uapi/mod.rs:113,203`), which calls **`send_sync` → `blocking_send` + `blocking_recv`** waiting for a response (`uapi/mod.rs:89-97`)
- the response is produced by `handle_api`, an **async task on the current-thread runtime** (`uapi/mod.rs:262`, `respond.send(...)`)

So:

```
runtime thread → blocked in Command::output() waiting for `wg` to exit
        `wg`   → blocked waiting for UAPI std-thread to write a reply
 UAPI thread   → blocking_recv, waiting for handle_api to answer
  handle_api   → needs the runtime thread to poll it … which is blocked in `wg`
```

A circular wait. The runtime thread is wedged forever, which means:

1. The `sigterm.recv()` / `sigint.recv()` arms (`:408`, `:404`) are never polled → **SIGTERM is silently ignored**.
2. The `control_socket_path.exists()` check (`:413`) never runs → socket removal wouldn't help either.
3. The device packet tasks (`handle_incoming`/`handle_outgoing`) can't be polled → **the tunnel passes no traffic**, and the routes it installed blackhole the LAN.

At disconnect, the privileged side dutifully `SIGTERM`s pid 78780, waits exactly 8 s, the wedged helper never moves, and you get `gotatun helper 78780 did not exit after SIGTERM; cleanup is incomplete` — the same pid surviving across both disconnect attempts. Cleanup (`cleanup_network`) never runs, so the routes + DNS overrides persist → LAN hijack → reboot. Same end state as before, different proximate failure — exactly your "different, but same system-broken result."
Extend `--mtu` support to include the userspace backend for `wgconf`.
Update the privileged helper to pass MTU overrides to `gotatun` and add
input validation for the MTU value. Update documentation with usage
details and macOS-specific troubleshooting for userspace data planes.
Implement a framed response protocol that allows the privileged service
to
capture log output during request processing and stream it to the CLI.
Updates the `gotatun` helper to write logs to a file that the service
tails, ensuring setup and teardown activity is reported back to the user
in real-time.
* fix/andi/cleanup_issue_mtu:
  Enable --mtu support for userspace WireGuard backends
Add `is_live()` to `ConnectionState` to verify if a tunnel process or
interface is still active. Automatically prune stale or corrupted state
files during connection attempts to prevent the system from getting
wedged in a "connected" state after a reboot.
# Conflicts:
#	src/userspace_helper.rs
Address review feedback on the reconcile tick:
- Skip (re-)applying tunnel DNS to a service whose original we failed to
  capture this tick, so we never overwrite DNS we cannot later restore.
- Keep a service's saved original when its restore fails, so teardown (or a
  later tick) can retry instead of stranding the user's DNS.
- Re-snapshot the fingerprint after mutating system DNS so the stored value
  matches reality and the next tick doesn't see a spurious diff.
…ng liveness

1. **`set_nonblocking(false)` on accepted sockets** (`privileged/mod.rs:145`): BSD/macOS inherits `O_NONBLOCK` from the listener to accepted connections, causing all `write_all()` calls to EAGAIN. Fixed.

2. **`wgconf status` stale-state false positive** (`wgconf/handlers.rs:153`): `cmd_status()` was printing "Connected:" from the state file without checking `is_live()`. After a reboot, the state file still exists but the tunnel is gone — so the autoconnect script saw "Connected:" and exited without reconnecting. Fixed by adding `&& conn.is_live()` to the filter.
Liveness was probed by an unprivileged Path::exists() on
/var/run/wireguard/<iface>.sock, but that dir is 0750 root:daemon —
so the check always returned false in user context (EACCES → exists()
== false). cmd_status reported "Not connected" on a live tunnel,
direct_connection_active() kept clearing "stale" state, and the
autoconnect agent reconnected every 60s. Helpers piled up on the shared
wgconf0.sock path (a departing helper's cleanup deleted the new helper's
socket → control_socket_removed self-kill), collapsing the tunnel.

- Add privileged InterfaceActive request; root answers the socket probe.
- Route userspace::is_interface_active through the privileged service.
- run_gotatun_up tears down an existing live helper before spawning,
  so there is never more than one and the socket can't be clobbered.
Copilot AI review requested due to automatic review settings June 17, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@pansen
pansen force-pushed the chore/andi/wg_solo branch from 94299c5 to 1ce3f48 Compare June 21, 2026 15:13
pansen added 5 commits June 22, 2026 09:33
Add an "Other tunnels" section listing foreign utun/VPN interfaces
(flagging 100.64/10 CGNAT as a likely Tailscale tailnet), a "System
resolver" line comparing the effective global resolver against the
tunnel's, and a tailscaled socket-presence hint — userspace Tailscale
has no utun of its own, so the interface scan alone would miss it.

Surfaces an interfering peer or a stale/foreign resolver at a glance
instead of needing manual ifconfig/scutil/netstat spelunking.
@pansen
pansen force-pushed the chore/andi/wg_solo branch 2 times, most recently from e49e31f to 92ec6f3 Compare July 5, 2026 13:51
@pansen
pansen force-pushed the chore/andi/wg_solo branch from 92ec6f3 to eb1282b Compare July 5, 2026 16:09
@pansen pansen self-assigned this Jul 5, 2026
@pansen
pansen merged commit 38b309b into main Jul 18, 2026
4 checks passed
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.

2 participants