Skip to content

Detect a game host that dies silently (proxy TCP keepalive + client idle probe) - #459

Merged
Delwing merged 2 commits into
masterfrom
claude/fix-issue-454
Oct 8, 2026
Merged

Delwing merged 2 commits into
masterfrom
claude/fix-issue-454

Conversation

@Delwing

@Delwing Delwing commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #454.

When the game host vanished without closing the connection (crash, power loss, network partition, NAT dropping the connection), the web client never noticed. There was no reconnect, and the next command was lost.

Changes

  • Proxy keepalive: the proxy now turns on TCP keepalive (60 s idle) on its game socket, for both plain and TLS connections. A dead host is detected in about 70 s and reported to the client as a WebSocket close, so the normal disconnect / sysDisconnectionEvent / auto-reconnect path runs.
  • Shared dialling code: connecting to the game moved into a new proxy/gameSocket.ts (connectGame / armGameKeepAlive), used by both dial paths.
  • Client idle probe: after 120 s with no game data, MudClient sends IAC NOP, and repeats while the silence lasts.
    • It only does this once the server has sent telnet on that socket, so a plain ws(s):// endpoint is never probed.
    • This covers direct WebSocket servers and proxies without keepalive, and keeps NAT entries alive.
    • The probe is never written to output or the session log.
  • Probe timer: reset by game output, and cleared on close, disconnect() and redial.
  • Tests: tests/mud/connection/idleProbe.test.ts and tests/proxy/gameKeepAlive.test.ts.

Known gaps

  • Cloudflare Worker proxy: worker/index.js uses the Workers connect(), which can't set keepalive. Worker users rely only on the client probe. That detects a dead host only after TCP stops retrying, which takes minutes after the first probe.
  • Lost command: a command typed during the detection window is still lost, as on desktop.

Testing

  • yarn typecheck passes, and proxy/ type-checks.
  • yarn check:vendored passes.
  • yarn test: 2 failures, neither caused by this branch:
    • serverEncoding.test.ts (GBK euro sign) also fails on master.
    • processingOrder384.test.ts (MCCP boundaries) fails only under load. It reproduces with master's MudClient.ts and passes when run alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Um3ZSHRbGj1Rbf55rAQzdM


Generated by Claude Code

The proxy dialled the game with net.connect/tls.connect and never turned
on TCP keepalive, so a peer that vanished without closing (crash, power
loss, partition, NAT drop) left the socket half-open until the next write.
The browser's WebSocket stayed healthy, nothing disconnected or
reconnected, and the player's next command was written into the dead
socket and lost.

- proxy: dial the game through connectGame() (proxy/gameSocket.ts), which
  arms keepalive with a 60 s idle delay on both the plain and TLS paths.
  Node probes 10 times at 1 s, so a dead peer raises ETIMEDOUT about 70 s
  after the last traffic; the proxy already turns that into a WebSocket
  close, which the client reports and auto-reconnects from.
- client: after 120 s of game silence, MudClient sends IAC NOP (repeating
  while the silence lasts), only to servers that have spoken telnet. That
  covers direct ws(s):// endpoints and proxies without keepalive: a peer
  that came back answers with a reset, one that is gone fails
  retransmission, and either way the socket closes through the normal
  disconnect path. The delay is past the bundled proxy's keepalive window
  so a probe never holds that off by leaving data unacknowledged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015dkC1X3wo4Y9wi9RoCrQpU
Comment thread proxy/gameSocket.ts Fixed
Moving tls.connect({ rejectUnauthorized: false }) into gameSocket.ts made
CodeQL report the existing, deliberate setting (the cert is checked by hand
in certProblems) as a new high alert. Leave the dial where it was and arm
keepalive on the socket instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Um3ZSHRbGj1Rbf55rAQzdM
@Delwing
Delwing merged commit 003ae7f into master Oct 8, 2026
10 checks passed
@Delwing
Delwing deleted the claude/fix-issue-454 branch October 8, 2026 23:18
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.

When the game host dies silently, web never notices: no reconnect, and the next command is lost (proxy has no TCP keepalive)

3 participants