Skip to content

Commit dcc33d6

Browse files
committed
feat(browser): bound tunnel route resources
1 parent 663c5b2 commit dcc33d6

8 files changed

Lines changed: 485 additions & 72 deletions

config/reliability-gates.jsonc

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,12 +2354,12 @@
23542354
"providers": ["local", "remote-runtime", "ssh", "wsl"],
23552355
"coveredPlatforms": ["macos"],
23562356
"coveredProviders": ["remote-runtime"],
2357-
"coverageNotes": "Deterministic protocol, registry, injected-socket, and loopback-listener tests cover strict framing, remote-DNS targets, exact destination-write and source-consumption credit, bounded receive bytes and chunks, authority epochs, exact host selection, one host per authenticated connection, four hosts per paired device, long-poll metering and disconnect abort, monotonic host/page/route generations without page tombstones, connection-owned cleanup, exact client revocation, stale and replaced fences, retired stream IDs, half-close and close ordering, SOCKS CONNECT, bind/close races, listener wildcard normalization, unsupported commands, unavailable routes, and raw/terminal binary-handler isolation. Explicitly injected browser-host and paired-runtime tunnel methods lease one exact host, then carry SOCKS and HTTP bytes over a dedicated E2EE socket to the fenced native execution-host revision and prove route close destroys the destination socket. Both methods and capabilities remain disabled in production. SSH and WSL providers remain uncovered.",
2357+
"coverageNotes": "Deterministic protocol, registry, injected-socket, and loopback-listener tests cover strict framing, remote-DNS targets, exact destination-write and source-consumption credit, at most 16 pending opens, 128 admitted opens per 10-second monotonic window, an 8 MiB aggregate application-buffer ledger across both traffic directions, bounded per-stream receive bytes and chunks, authority epochs, exact host selection, one host per authenticated connection, four hosts per paired device, long-poll metering and disconnect abort, monotonic host/page/route generations without page tombstones, connection-owned cleanup, exact client revocation, stale and replaced fences, retired stream IDs, half-close and close ordering, SOCKS CONNECT, bind/close races, listener wildcard normalization, unsupported commands, unavailable routes, and raw/terminal binary-handler isolation. Explicitly injected browser-host and paired-runtime tunnel methods lease one exact host, then carry SOCKS and HTTP bytes over a dedicated E2EE socket to the fenced native execution-host revision and prove route close destroys the destination socket. Both methods and capabilities remain disabled in production. Encrypted transport buffers, browser-host/process ledgers, SSH, and WSL remain uncovered.",
23582358
"motivatingLinks": [
23592359
"https://linear.app/stably/issue/STA-4150/refactor-remote-browser-to-client-hosted-electron-webviews"
23602360
],
2361-
"invariant": "A client-hosted browser network route exists only for an exact live server-owned browser-host lease, authority epoch, host generation, paired identity, execution-host revision, and server-owned route generation. Host selection never chooses arbitrarily, stale cleanup cannot remove a replacement, destination names remain unresolved until the execution host, credit returns only after writes settle, and route loss never falls back to desktop DNS or sockets. The local SOCKS endpoint accepts only loopback CONNECT and rejects unsupported commands.",
2362-
"oracle": "Attach two browser hosts and require unqualified selection to fail ambiguous while exact selection succeeds. Reject a second identity on one connection and a fifth identity for one paired device, release one exact lease, then admit its replacement without starving another device. Saturate the browser-host long-poll sub-cap, require an ordinary wait to remain admitted, close the socket, and prove both counters and handlers release. Replace one same-device connection, require the old subscription and route to fence, then prove late old cleanup leaves the new generation live. Retire a closed page placement by exact token, reject delayed cleanup against a replacement and server placement, and require its reused ID to receive a higher global generation without retaining a tombstone. Reject missing, stale, wrong-device, wrong-epoch, and wrong-execution-revision tunnel requests before binary registration. Drive the accepted lease through a real paired E2EE SOCKS-to-HTTP journey, then exercise exact frame, credit, half-close, stale-stream, remote-DNS, unsupported-command, offline, and teardown assertions.",
2361+
"invariant": "A client-hosted browser network route exists only for an exact live server-owned browser-host lease, authority epoch, host generation, paired identity, execution-host revision, and server-owned route generation. Host selection never chooses arbitrarily, stale cleanup cannot remove a replacement, destination names remain unresolved until the execution host, credit returns only after writes settle, and route loss never falls back to desktop DNS or sockets. Pending opens, admitted open rate, and session-retained application bytes remain bounded with exact release on settlement and retirement. The local SOCKS endpoint accepts only loopback CONNECT and rejects unsupported commands.",
2362+
"oracle": "Attach two browser hosts and require unqualified selection to fail ambiguous while exact selection succeeds. Reject a second identity on one connection and a fifth identity for one paired device, release one exact lease, then admit its replacement without starving another device. Saturate the browser-host long-poll sub-cap, require an ordinary wait to remain admitted, close the socket, and prove both counters and handlers release. Replace one same-device connection, require the old subscription and route to fence, then prove late old cleanup leaves the new generation live. Retire a closed page placement by exact token, reject delayed cleanup against a replacement and server placement, and require its reused ID to receive a higher global generation without retaining a tombstone. Admit 16 pending destination opens and reject the seventeenth until one connects; admit 128 opens in one monotonic 10-second window and reject the 129th until the window expires. Fill 8 MiB across destination-to-client queues and separately across unsettled client-to-destination writes, release one exact retired stream, admit one replacement, and require the next byte claim to close the route without stale-callback or reentrant-close underflow. Reject missing, stale, wrong-device, wrong-epoch, and wrong-execution-revision tunnel requests before binary registration. Drive the accepted lease through a real paired E2EE SOCKS-to-HTTP journey, then exercise exact frame, credit, half-close, stale-stream, remote-DNS, unsupported-command, offline, and teardown assertions.",
23632363
"commands": [
23642364
"pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-network-capabilities.test.ts src/shared/browser-network-tunnel-protocol.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
23652365
"pnpm exec vitest run --config config/vitest.config.ts src/main/browser/browser-network-tunnel-client.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/runtime-binary-message-router.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts",
@@ -2423,7 +2423,10 @@
24232423
"file": "src/main/browser/browser-network-tunnel-session.test.ts",
24242424
"assertions": [
24252425
"destination writes replenish client credit only after settlement",
2426-
"stale generations and reused stream IDs cannot replace the current destination"
2426+
"stale generations and reused stream IDs cannot replace the current destination",
2427+
"pending opens and admitted open rate remain bounded and recover only after exact release or window expiry",
2428+
"both traffic directions share an 8 MiB application-retention ledger with exact settlement, retirement, stale-callback, and reentrant-close release",
2429+
"unsettled client-to-destination writes retain at most 256 chunks per stream independently of byte credit"
24272430
]
24282431
},
24292432
{
@@ -2483,6 +2486,15 @@
24832486
}
24842487
],
24852488
"evidenceRuns": [
2489+
{
2490+
"date": "2026-08-14",
2491+
"runner": "local",
2492+
"platform": "macos",
2493+
"command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/browser-client-host-protocol.test.ts src/main/runtime/browser-host-lease-registry.test.ts src/main/runtime/runtime-rpc-browser-host-admission.test.ts src/main/runtime/rpc/methods/browser-client-host.test.ts src/main/runtime/rpc/methods/browser-network-tunnel.test.ts src/main/browser/paired-runtime-browser-host-lease.test.ts src/main/browser/paired-runtime-browser-network-route.test.ts src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts src/main/browser/browser-network-tunnel-client.test.ts src/main/browser/browser-network-tunnel-session.test.ts src/main/browser/remote-browser-socks-server.test.ts",
2494+
"result": "passed",
2495+
"durationSeconds": 6.9,
2496+
"summary": "Eleven files passed 73 admission, lease, route-generation, bidirectional retained-byte/chunk, pending-open/rate, exact-release, SOCKS, and real paired E2EE tests; the full runtime-RPC suite separately passed 1,347 tests with one skip."
2497+
},
24862498
{
24872499
"date": "2026-08-14",
24882500
"runner": "local",
@@ -2530,14 +2542,14 @@
25302542
},
25312543
"redGreenEvidence": {
25322544
"status": "partial",
2533-
"evidence": "The protocol and tunnel-session oracles fail on origin/main 3246b73add because the modules do not exist, then pass on the candidate. The lease registry and control-client suites were recorded red at missing modules before implementation. The unchanged tunnel authorization oracle was behaviorally red because an authenticated socket could self-assert its paired device ID and received ready generation 7 without a lease; it is green after server-owned lease and route fencing. On c9a41abf24, the unchanged admission oracle failed three ways: a second identity on one connection and a fifth identity for one paired device were both admitted, and closed page placement had no retirement API. All three pass after bounded admission and tombstone-free global page generations. The real paired journey failed at the missing network.browserTunnel method before explicit test injection, then passed with an exact control lease while both production registries remained closed. Full mutation/revert evidence remains to be collected before promotion."
2545+
"evidence": "The protocol and tunnel-session oracles fail on origin/main 3246b73add because the modules do not exist, then pass on the candidate. The lease registry and control-client suites were recorded red at missing modules before implementation. The unchanged tunnel authorization oracle was behaviorally red because an authenticated socket could self-assert its paired device ID and received ready generation 7 without a lease; it is green after server-owned lease and route fencing. On c9a41abf24, the unchanged admission oracle failed three ways: a second identity on one connection and a fifth identity for one paired device were both admitted, and closed page placement had no retirement API. All three pass after bounded admission and tombstone-free global page generations. On parent 392d13be9f, the deterministic resource oracle admitted 17 pending opens, admitted a 129th open inside 10 seconds, and retained more than 8 MiB across one route; all three failed assertions pass on the candidate, which also covers unsettled writes and reentrant exact release. The real paired journey failed at the missing network.browserTunnel method before explicit test injection, then passed with an exact control lease while both production registries remained closed. Full mutation/revert evidence remains to be collected before promotion."
25342546
},
25352547
"performanceBudget": {
25362548
"required": true,
2537-
"evidence": "Data frames, send credit, pending bytes and chunk counts in both directions, streams, SOCKS handshake and pipelined application bytes are bounded. One authenticated connection retains at most one browser host and one paired device at most four, with exact release restoring admission. Permanent browser-host polls use at most one quarter of the shared long-poll slots, and real injected dispatch proves an ordinary wait remains admitted and socket close releases both classes. Credit returns only after the execution-host or browser-facing socket write settles, and the injected transport must explicitly accept each frame. Pending-open/rate admission, aggregate route/browser-host/process byte ledgers, and drain-aware fair scheduling remain required before live tunnel advertisement."
2549+
"evidence": "Data frames, send credit, pending bytes and chunk counts in both directions, streams, SOCKS handshake, and pipelined application bytes are bounded. Each route admits at most 16 pending opens, 128 opens per monotonic 10-second window, and 8 MiB of session-owned queued or unsettled application bytes with exact settlement and retirement release. One authenticated connection retains at most one browser host and one paired device at most four, with exact release restoring admission. Permanent browser-host polls use at most one quarter of the shared long-poll slots, and real injected dispatch proves an ordinary wait remains admitted and socket close releases both classes. Credit returns only after the execution-host or browser-facing socket write settles, and the injected transport must explicitly accept each frame. Encrypted WebSocket/native buffers, aggregate browser-host/process ledgers, and drain-aware fair scheduling remain required before live tunnel advertisement."
25382550
},
25392551
"promotionCriteria": [
2540-
"Add bounded pending-open admission and open-rate limits plus aggregate route, browser-host, and process memory ledgers with runtime-wide fair scheduling.",
2552+
"Complete aggregate accounting across encrypted transport/native buffers, browser hosts, and the process with runtime-wide fair scheduling.",
25412553
"Approve all same-host local processes and users as inside the desktop trust boundary or replace SOCKS no-auth with Chromium-compatible process isolation.",
25422554
"Add old/new peer coverage to the dedicated paired-runtime binary tunnel journey.",
25432555
"Add SSH2, system SSH, WSL, Linux, and physical Windows evidence.",
@@ -2546,7 +2558,7 @@
25462558
"knownGaps": [
25472559
"No runtime capability is advertised, the paired tunnel method is not registered in production, and no Electron webview selects it yet.",
25482560
"The token-safe page-retirement API has no production lifecycle caller until client placement activates.",
2549-
"Pending-open and open-rate admission plus aggregate encrypted-WebSocket and destination-buffer accounting are not implemented.",
2561+
"Session-owned application buffers are bounded, but encrypted WebSocket/native buffers and aggregate browser-host/process accounting are not implemented.",
25502562
"The loopback SOCKS endpoint uses Chromium-compatible no-auth and is not safe to activate until its desktop trust boundary is approved or isolated.",
25512563
"SSH, WSL, browserless serve, Electron proxy policy, UDP denial, and desktop DNS/socket capture are not exercised."
25522564
],

src/main/browser/browser-network-tunnel-destination-flow.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,25 @@ type BrowserNetworkTunnelDestinationFlowActions = {
1414
sendData: (bytes: Uint8Array<ArrayBufferLike>) => boolean
1515
sendHalfClose: () => void
1616
finalizeClose: () => void
17+
releaseRetainedBytes: (bytes: number) => void
18+
}
19+
20+
export function halfCloseBrowserNetworkDestination(
21+
stream: BrowserNetworkTunnelStream
22+
): string | null {
23+
if (!stream.connected || stream.clientEnded) {
24+
return 'invalid_client_half_close'
25+
}
26+
stream.clientEnded = true
27+
stream.socket.end()
28+
return null
1729
}
1830

1931
export function writeBrowserNetworkDestination(
2032
stream: BrowserNetworkTunnelStream,
2133
payload: Uint8Array<ArrayBufferLike>,
22-
onSettled: (bytes: number) => void
34+
onSettled: (bytes: number) => void,
35+
claimRetainedBytes: (bytes: number) => (() => void) | null
2336
): string | null {
2437
if (!stream.connected || stream.clientEnded) {
2538
return 'invalid_client_data'
@@ -30,9 +43,31 @@ export function writeBrowserNetworkDestination(
3043
if (payload.byteLength === 0) {
3144
return null
3245
}
46+
if (
47+
stream.pendingDestinationWriteReleases.size >= BROWSER_NETWORK_TUNNEL_MAX_PENDING_SOCKET_CHUNKS
48+
) {
49+
return 'destination_write_chunk_overflow'
50+
}
51+
const releaseClaim = claimRetainedBytes(payload.byteLength)
52+
if (!releaseClaim) {
53+
return 'route_buffer_overflow'
54+
}
3355
stream.receiveCredit -= payload.byteLength
3456
const bytes = payload.slice()
35-
stream.socket.write(bytes, () => onSettled(bytes.byteLength))
57+
let retained = true
58+
const release = (): void => {
59+
if (!retained) {
60+
return
61+
}
62+
retained = false
63+
stream.pendingDestinationWriteReleases.delete(release)
64+
releaseClaim()
65+
}
66+
stream.pendingDestinationWriteReleases.add(release)
67+
stream.socket.write(bytes, () => {
68+
release()
69+
onSettled(bytes.byteLength)
70+
})
3671
return null
3772
}
3873

@@ -84,8 +119,12 @@ export function flushBrowserNetworkDestination(
84119
if (!actions.sendData(next.subarray(0, length))) {
85120
return
86121
}
122+
if (!actions.isCurrent()) {
123+
return
124+
}
87125
stream.sendCredit -= length
88126
stream.pendingToClientBytes -= length
127+
actions.releaseRetainedBytes(length)
89128
if (length === next.byteLength) {
90129
stream.pendingToClient.shift()
91130
} else {

src/main/browser/browser-network-tunnel-frame-sender.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2+
BrowserNetworkTunnelOpcode,
23
encodeBrowserNetworkTunnelFrame,
3-
type BrowserNetworkTunnelOpcode,
44
type BrowserNetworkTunnelFrame
55
} from '../../shared/browser-network-tunnel-protocol'
66
import type { BrowserNetworkTunnelSessionOptions } from './browser-network-tunnel-stream-state'
@@ -43,4 +43,8 @@ export class BrowserNetworkTunnelFrameSender {
4343
}
4444
return accepted
4545
}
46+
47+
sendError(streamId: number, code: string): boolean {
48+
return this.send(BrowserNetworkTunnelOpcode.Error, streamId, new TextEncoder().encode(code))
49+
}
4650
}

0 commit comments

Comments
 (0)