Repository navigation
fix(relay): harden the machine doors #2073 put on the hosted relay - #2076
Merged
Merged
Conversation
…ut loud Review of #2073 found the machine doors abusable once they went live on the hosted relay. Relay streams had no deadlines, so the serving machine's 30-second handshake limit was a no-op: 16 silent dials held every slot, and a lone "pair" byte burned the code and froze pairing. muxStream now carries read/write deadlines, so both end at HandshakeWithin and the next code is offered. Also: the self-hosted relay lets only the same key replace a silent registration; serve backs off until a registration has held a minute, names a throttled relay in a sentence, and answers "lost sync" with one "back — reachable as <name> again"; the WebSocket upgrade honours HTTP(S)_PROXY and leaves no idle connection behind on a 426; a non-JSON or HTML body becomes a one-sentence fact naming the relay with its base path; an older relay's 503 ceiling reads as busy; the carrier keeps its read limit, checks the negotiated subprotocol and splits writes over the relay's message ceiling; serve --help and the manual name the full relay precedence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…their keys The hosted machine doors kept their rate limits per name, inside each name's Durable Object, so one address could hit unlimited names and wake an object for each: no cost ceiling under a deliberate flood. Dials and registrations are now limited per caller network at the Worker front, with Cloudflare's rate-limiting binding (30 dials, 10 registrations a minute, the self-hosted relay's numbers) and a bounded in-memory fallback, and only names nameFor can produce reach an object at all. A registration with a different key can no longer evict a live machine during a stall: only the same key may replace a silent socket. Sends that can throw are guarded so a dying socket cannot leak a stream slot, and pending sockets that never prove their key are swept. The room logic moves to room.js so Node's test runner can exercise it without a Worker runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #2073, whose machine doors went live on the production relay on 2026-10-30. A review found them abusable; this fixes every confirmed finding.
What was wrong → what is true now
Pbyte burned the pairing code and froze pairingmuxStreamhas read/write deadlines; silent dials end at 30 s and the next code is offered[[ratelimits]], 30 dials / 10 registrations a minute), and only realnameFornames reach an objectservereset its backoff the instantRegisterreturned (1 s reconnect storm) and printed raw sentinelslost syncis answered byback — reachable as <name> againHTTP(S)_PROXY, leaked a connection per426, showed raw HTML bodies, read an old relay's 503 as unreachable, dropped the read limitserve --helpand the manual omitted the savedv3/remote/relayfile from relay precedenceProof
Hand test — real
codeaf serve+chat --atthrough the hosted Worker underwrangler dev(BASE_PATH=/fabric), attacked with the real relay client:--at), a probe at 6 s was refused, all 15 closed by the machine at 30 s, a probe at 46 s got through.Pbyte: ended at 30 s, machine saidsomeone typed a wrong code, so that code is no longer goodand printed a fresh code.zzz-zzz-99,a-a-00,otter-lamp-05):that is not a machine nameat the front door.lost sync — trying againonce, no spam; relay back:back — reachable as chalk-candle-50 again8 s later, and dials reach it.Tests:
relay/hosted248/248;internal/relay,internal/pair,internal/relayserve,internal/manual, focusedcmd/codeaf(Serve|Pair|Relay|At),make test-laws— all green.Deploy
Additive: one new
[[ratelimits]]pair, no migration. After merge, the production relay is redeployed fromdev(the automatic per-channel deploy is a separate PR).🤖 Generated with Claude Code