Multi-tenant reverse-tunnel service ("ngrok for Claude Code" with E2E): a
host-agent dials OUT to an operator-run relay; external devices reach the host
THROUGH the relay, routed by per-tenant subdomain, forwarding ciphertext only
(the relay never sees plaintext). Lets a customer reach their own self-hosted
web-terminal from anywhere with zero networking setup.
Packages — all tsc-strict + vitest green (656 tests), cross-package integration verified:
- relay-contracts: frozen shared contracts (mux frame codec, data model,
capability token, E2E envelope, pairing) — the src/types.ts analog
- term-relay: native WS mux + stateless data plane (subdomain routing, ciphertext forward)
- agent: host-agent (pairing, per-host Ed25519 + mTLS dial-out, forwards to 127.0.0.1:3000)
- control-plane: accounts/hosts registry, pairing-code flow, routing table, provisioning
- relay-e2e: browser<->agent E2E (X25519 ECDH through relay, AEAD, anti-replay, recoverable replay key)
- relay-auth: Passkey/WebAuthn, capability tokens, per-host certs, deny-by-default tenant isolation
- relay-web: browser login + Web Crypto E2E + client-side preview rendering
Security invariants INV1-15 enforced; cross-tenant isolation CI tripwire live
(.github/workflows/relay-tripwire.yml). Design + implementation-level plans in
docs/PLAN_RELAY_*.md and docs/EXPLORE_RELAY_SERVICE.md.
NOTE: generated autonomously per the reviewed plans. The security-critical
packages (relay-e2e, relay-auth) REQUIRE expert security audit before any real
deployment — passing tests prove self-consistency, not resistance to attackers.
Base app (src/, public/) unchanged; concurrent desktop work left uncommitted.
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
import { describe, test, expect, vi } from 'vitest'
|
|
import { createMuxSession, type MuxSession, type MuxStreamHandle } from '../mux/mux-session.js'
|
|
import { encodeMuxFrame, decodeHeader, type MuxOpen } from '../mux/frame-codec.js'
|
|
import type { ScheduleHandle } from '../mux/heartbeat.js'
|
|
|
|
const noAutoSchedule = (): { schedule: (fn: () => void) => ScheduleHandle; tick: () => void } => {
|
|
let scheduled: (() => void) | null = null
|
|
return {
|
|
schedule: (fn) => {
|
|
scheduled = fn
|
|
return { cancel: () => (scheduled = null) }
|
|
},
|
|
tick: () => scheduled?.(),
|
|
}
|
|
}
|
|
|
|
function openFor(streamId: number): MuxOpen {
|
|
return {
|
|
streamId,
|
|
subdomain: 'alice',
|
|
requestPath: '/term',
|
|
originHeader: 'https://alice.term.example.com',
|
|
remoteAddrHash: 'hash',
|
|
capabilityTokenRef: 'jti-1',
|
|
}
|
|
}
|
|
|
|
interface Pair {
|
|
relay: MuxSession
|
|
agent: MuxSession
|
|
opened: { open: MuxOpen; stream: MuxStreamHandle }[]
|
|
agentData: { streamId: number; payload: Uint8Array }[]
|
|
}
|
|
|
|
function wirePair(relayWindow = 1_000_000, agentWindow = 1_000_000): Pair {
|
|
const opened: Pair['opened'] = []
|
|
const agentData: Pair['agentData'] = []
|
|
let agent!: MuxSession
|
|
const relay = createMuxSession({
|
|
role: 'relay',
|
|
sendWire: (f) => agent.onWire(f),
|
|
onOpen: () => {},
|
|
onData: () => {},
|
|
onClose: () => {},
|
|
onDead: () => {},
|
|
maxFrameBytes: 1024,
|
|
initialWindowBytes: relayWindow,
|
|
schedule: noAutoSchedule().schedule,
|
|
})
|
|
agent = createMuxSession({
|
|
role: 'agent',
|
|
sendWire: (f) => relay.onWire(f),
|
|
onOpen: (open, stream) => opened.push({ open, stream }),
|
|
onData: (streamId, payload) => agentData.push({ streamId, payload }),
|
|
onClose: () => {},
|
|
onDead: () => {},
|
|
maxFrameBytes: 1024,
|
|
initialWindowBytes: agentWindow,
|
|
schedule: noAutoSchedule().schedule,
|
|
})
|
|
return { relay, agent, opened, agentData }
|
|
}
|
|
|
|
describe('mux session (T6)', () => {
|
|
test('openStream → agent onOpen with a fresh monotonic streamId; never reused', () => {
|
|
const p = wirePair()
|
|
p.relay.openStream(openFor(0))
|
|
p.relay.openStream(openFor(0))
|
|
expect(p.opened.map((o) => o.open.streamId)).toEqual([1, 2])
|
|
})
|
|
|
|
test('writeData delivers a byte-identical OPAQUE payload (INV2), never inspected', () => {
|
|
const p = wirePair()
|
|
const h = p.relay.openStream(openFor(0))
|
|
const marker = new TextEncoder().encode('PLAINTEXT-MARKER-🔒')
|
|
expect(h.writeData(marker)).toBe(true)
|
|
expect(p.agentData).toHaveLength(1)
|
|
expect([...p.agentData[0]!.payload]).toEqual([...marker])
|
|
})
|
|
|
|
test('backpressure: writeData returns false when the window is full; WINDOW_UPDATE resumes', () => {
|
|
// relay send window = 10, agent receive window huge (no auto-replenish crossing threshold).
|
|
const p = wirePair(10, 1_000_000)
|
|
const h = p.relay.openStream(openFor(0))
|
|
const streamId = h.streamId
|
|
const a = new Uint8Array(8).fill(1)
|
|
const b = new Uint8Array(8).fill(2)
|
|
expect(h.writeData(a)).toBe(true) // window 10 → 2
|
|
expect(h.writeData(b)).toBe(false) // 8 > 2 → buffered
|
|
expect(p.agentData).toHaveLength(1)
|
|
// Simulate a peer WINDOW_UPDATE granting credit → buffered `b` flushes.
|
|
p.relay.onWire(encodeWU(streamId, 50))
|
|
expect(p.agentData).toHaveLength(2)
|
|
expect([...p.agentData[1]!.payload]).toEqual([...b])
|
|
})
|
|
|
|
test('per-stream isolation: exhausting stream A does not block stream B', () => {
|
|
const p = wirePair(10, 1_000_000)
|
|
const a = p.relay.openStream(openFor(0))
|
|
const b = p.relay.openStream(openFor(0))
|
|
expect(a.writeData(new Uint8Array(10).fill(1))).toBe(true) // A window exhausted
|
|
expect(a.writeData(new Uint8Array(1))).toBe(false)
|
|
expect(b.writeData(new Uint8Array(10).fill(2))).toBe(true) // B unaffected
|
|
})
|
|
|
|
test('inbound DATA for an unknown stream → CLOSE+RST for that stream only (tunnel stays up)', () => {
|
|
const wire: Uint8Array[] = []
|
|
const s = createMuxSession({
|
|
role: 'agent',
|
|
sendWire: (f) => wire.push(f),
|
|
onOpen: () => {},
|
|
onData: () => {},
|
|
onClose: () => {},
|
|
onDead: () => {},
|
|
maxFrameBytes: 1024,
|
|
initialWindowBytes: 1000,
|
|
schedule: noAutoSchedule().schedule,
|
|
})
|
|
s.onWire(encodeMuxFrame({ version: 1, type: 'data', fin: false, rst: false, streamId: 99, payloadLen: 1 }, new Uint8Array([7])))
|
|
const rst = wire.map(decodeHeader).find((x) => x.type === 'close' && x.rst)
|
|
expect(rst?.streamId).toBe(99)
|
|
})
|
|
|
|
test('frame ceiling: inbound payloadLen > maxFrameBytes → RST that stream, no OOM', () => {
|
|
const wire: Uint8Array[] = []
|
|
const s = createMuxSession({
|
|
role: 'agent',
|
|
sendWire: (f) => wire.push(f),
|
|
onOpen: () => {},
|
|
onData: () => {},
|
|
onClose: () => {},
|
|
onDead: () => {},
|
|
maxFrameBytes: 4,
|
|
initialWindowBytes: 1000,
|
|
schedule: noAutoSchedule().schedule,
|
|
})
|
|
const oversized = encodeMuxFrame({ version: 1, type: 'data', fin: false, rst: false, streamId: 5, payloadLen: 5 }, new Uint8Array(5))
|
|
s.onWire(oversized)
|
|
const rst = wire.map(decodeHeader).find((x) => x.rst)
|
|
expect(rst?.streamId).toBe(5)
|
|
})
|
|
|
|
test('heartbeat: no PONG for the miss limit ⇒ onDead', () => {
|
|
const sched = noAutoSchedule()
|
|
const onDead = vi.fn()
|
|
const s = createMuxSession({
|
|
role: 'relay',
|
|
sendWire: () => {}, // peer never responds
|
|
onOpen: () => {},
|
|
onData: () => {},
|
|
onClose: () => {},
|
|
onDead,
|
|
maxFrameBytes: 1024,
|
|
initialWindowBytes: 1000,
|
|
schedule: sched.schedule,
|
|
})
|
|
s.openStream(openFor(0)) // starts heartbeat (ping #1)
|
|
sched.tick() // miss 1
|
|
sched.tick() // miss 2
|
|
sched.tick() // miss 3 → dead
|
|
expect(onDead).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
function encodeWU(streamId: number, credit: number): Uint8Array {
|
|
const payload = new Uint8Array(4)
|
|
new DataView(payload.buffer).setUint32(0, credit, false)
|
|
return encodeMuxFrame(
|
|
{ version: 1, type: 'windowUpdate', fin: false, rst: false, streamId, payloadLen: 4 },
|
|
payload,
|
|
)
|
|
}
|