Skip to content

A 15-minute network drop permanently destroys a share, with the process still running and no way to recover #21

Description

@artemiia

If the machine hosting a share loses its network for more than fifteen minutes — a laptop sleeping over lunch, a WiFi switch, a VPN reconnect — the relay permanently deletes the session, and the CLI can never get it back. The wrapped process keeps running the whole time, completely unaware. Every link you handed out is dead, and the only way to notice is to run shell list and spot that the relay column now says expired.

The reason is that a disconnect collapses the session's remaining life from twelve hours to fifteen minutes. When the host socket closes, handleSocketEnd rewrites the expiry:

this.meta.status = "disconnected";
this.meta.expiresAt = Date.now() + (this.meta.persistent ? PERSISTENT_TTL_MS : DISCONNECTED_GRACE_MS);

with DISCONNECTED_GRACE_MS = 15 * 60 * 1000 (worker/index.ts:42). A durable alarm then deletes the session when that window elapses.

The fifteen-minute grace is a defensible design choice on its own. What makes this a bug is that there is no way back. The host's relay layer retries forever with a ten-second backoff cap (internal/relay/relay.go), but it only ever reconnects to a session ID — it never re-registers. Once the relay has deleted that ID, the CLI is dialling something that no longer exists, indefinitely, while holding a live PTY and valid host credentials. Nothing surfaces the failure to the person who created the share.

Reproduction on production

Verified against the live relay, not just locally. A TLS-terminating proxy sat between the CLI and shell.online so the host's connection could be cut on demand while the relay stayed up — the same shape as a sleeping laptop. The session itself was a genuine production session, confirmed by querying https://shell.online/api/sessions/<id> directly rather than through the proxy.

baseline                {"exists":true,"status":"connected"}     200
--- host connection cut ---
t+0                     {"exists":true,"status":"disconnected"}  200
t+3m                    {"exists":true,"status":"disconnected"}  200
t+13m                   {"exists":true,"status":"disconnected"}  200
t+15m                   {"exists":false}                         404   <- deleted
t+20m                   {"exists":false}                         404
--- host connection restored ---
+25s                    {"exists":false}                         404
+125s                   {"exists":false}                         404

Host side, throughout and afterwards:

$ shell list
bEnHSSRWDz  22m49s  expired  on exit  interactive+e2ee  sleep 4000  https://shell.online/s/bEnHSS...

The process is still alive at 22 minutes. The link is gone for good.

--persistent is immune, which confirms the mechanism

The same outage against a persistent session (run locally with the grace window shortened to 20s so the boundary is easy to cross):

t+10s … t+50s   {"exists":true,"status":"disconnected"}
after restore   {"exists":true,"status":"connected"}   ->  shell list: online

Persistent sessions get PERSISTENT_TTL_MS on disconnect instead of the grace window, and there is a /api/sessions/resume path for re-registering. Default sessions have neither.

What was ruled out

  • SIGSTOP on the host does not reproduce it. Freezing the process leaves the TCP socket open, so the relay still reports connected — confirmed for four minutes on production. Only an actual connection break triggers it.
  • Not the 12-hour TTL. Far too long to explain links dying inside a working day.
  • Not the client giving up. internal/relay/relay.go never stops retrying; it simply has nothing to reconnect to.

Impact

This is invisible and unrecoverable. The owner gets no notification, the process keeps consuming resources, and recipients see "Session ended. This sharing link no longer exists." Anyone who shares a link and then closes their laptop for a meeting comes back to a dead link and a running process. Because the trigger is ordinary laptop behaviour rather than anything the user did to the session, it reads as links going offline at random.

Suggested fixes

  • Let the host re-register after its session has been reaped. The CLI still holds the host credential; an equivalent of /api/sessions/resume for non-persistent sessions would make the existing infinite reconnect loop actually able to succeed.
  • Failing that, have the CLI detect a 404/410 on reconnect, stop looping, and tell the owner the share is gone — ideally offering to reissue.
  • Consider extending or making the grace window configurable; fifteen minutes is shorter than a lunch break.
  • Related but distinct: shell list shows a session as running after the relay has already expired it #10 covered shell list misreporting an expired session as running. That reporting is now correct — this issue is about the session being destroyed and unrecoverable in the first place.

Environment

shell 0.7.3 (latest; matches release.json and the GitHub latest release), macOS 26.2 arm64, against the production shell.online relay. Worker behaviour cross-checked in the v0.7.3 source tree.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions