Research: canvas as a data-plane client on any node (browser and passkey feasibility) #167

Closed
opened 2026-10-11 14:45:43 +00:00 by kayg · 21 comments
Owner

Owner idea 2026-10-11: split the server into a control plane (identity, policy, CA, audit, canvas layouts) and a data plane where any node can serve the canvas and carry session traffic directly over the mesh (end-to-end), with every permission enforced at the target host. Research first: browser behaviours and blockers (page on the server origin talking to a local infraternal agent; a canvas served from localhost; passkeys across those origins) in Chromium, Firefox and WebKit/Safari, desktop and iOS. Owner decides after the numbers (foundational: touches D8-D10).

Owner idea 2026-10-11: split the server into a control plane (identity, policy, CA, audit, canvas layouts) and a data plane where any node can serve the canvas and carry session traffic directly over the mesh (end-to-end), with every permission enforced at the target host. Research first: browser behaviours and blockers (page on the server origin talking to a local infraternal agent; a canvas served from localhost; passkeys across those origins) in Chromium, Firefox and WebKit/Safari, desktop and iOS. Owner decides after the numbers (foundational: touches D8-D10).
Author
Owner

Started research on branch research-canvas-node, based on origin/dev at ded3abd609e5290a7027b6888ecd9864a2d30374. Issue #167 has 0 comments; fj issue view exposes its body and comments but no parent relation, so no parent issue was identifiable through the approved CLI.

Started research on branch `research-canvas-node`, based on `origin/dev` at `ded3abd609e5290a7027b6888ecd9864a2d30374`. Issue #167 has 0 comments; `fj issue view` exposes its body and comments but no parent relation, so no parent issue was identifiable through the approved CLI.
Author
Owner

Orchestrator, owner idea 2026-10-11 (add to the research): canvas nodes served on a real HTTPS name by the reverse-proxy plugin in the infraternal agent, e.g. https://. (ACME DNS-01 cert), resolving to a LAN address at home. Evaluate: sign-in redirect to the server origin and back with a short-lived per-origin session (works in Chromium/Firefox/WebKit?), passkey RP-ID implications (passkeys stay on the server origin; Related Origin Requests not needed?), iOS Safari/PWA reaching a LAN-resolving public name, mixed private-IP DNS answers (DNS rebinding protections in routers/resolvers that drop private answers), and serving a signed UI bundle that must match the control-plane version. Compare with the localhost variants.

Orchestrator, owner idea 2026-10-11 (add to the research): canvas nodes served on a real HTTPS name by the reverse-proxy plugin in the infraternal agent, e.g. https://<node>.<owner-domain> (ACME DNS-01 cert), resolving to a LAN address at home. Evaluate: sign-in redirect to the server origin and back with a short-lived per-origin session (works in Chromium/Firefox/WebKit?), passkey RP-ID implications (passkeys stay on the server origin; Related Origin Requests not needed?), iOS Safari/PWA reaching a LAN-resolving public name, mixed private-IP DNS answers (DNS rebinding protections in routers/resolvers that drop private answers), and serving a signed UI bundle that must match the control-plane version. Compare with the localhost variants.
Author
Owner

Chrome finding (Linux Debian 13; Google Chrome Stable 155.0.8059.39 via Playwright 1.64.0, headless): from the secure test origin https://8.8.8.8:4844, local-network and loopback-network query as prompt. Fetches to http://127.0.0.1:4841 and http://localhost:4841 fail before reaching the fixture; console reports loopback permission denied. ws://127.0.0.1:4842, ws://localhost:4842, and wss://localhost:4843 fail with ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS; the fixture records no requests or PNA OPTIONS. Headless mode showed no visible prompt; Chrome’s documented prompt text is “Look for and connect to any device on your local network.” Raw evidence: research/canvas-node/results/browser-probes.json. This differs from Chrome’s Chrome-138 rollout note, which said WebSockets were not yet gated.

Chrome finding (Linux Debian 13; Google Chrome Stable 155.0.8059.39 via Playwright 1.64.0, headless): from the secure test origin `https://8.8.8.8:4844`, `local-network` and `loopback-network` query as `prompt`. Fetches to `http://127.0.0.1:4841` and `http://localhost:4841` fail before reaching the fixture; console reports loopback permission denied. `ws://127.0.0.1:4842`, `ws://localhost:4842`, and `wss://localhost:4843` fail with `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS`; the fixture records no requests or PNA OPTIONS. Headless mode showed no visible prompt; Chrome’s documented prompt text is “Look for and connect to any device on your local network.” Raw evidence: `research/canvas-node/results/browser-probes.json`. This differs from Chrome’s Chrome-138 rollout note, which said WebSockets were not yet gated.
Author
Owner

Firefox/WebKit finding (Linux, headless Playwright): Firefox 157.0 reports local-network and loopback-network as prompt; fetches stayed pending until the harness aborted at 1.8 s, WebSockets timed out, and the local fixture saw no requests. WebKit 27.2 reports the same permission states but sends no local HTTP or ws:// request; console says “Not allowed to request resource.” Its wss://localhost:4843 probe did reach the fixture and echoed one byte without a visible prompt. Headless runs cannot show browser-chrome permission UI, so these are permission states and observed outcomes, not visual confirmation of a dialog. Evidence: research/canvas-node/results/browser-probes.json.

Firefox/WebKit finding (Linux, headless Playwright): Firefox 157.0 reports `local-network` and `loopback-network` as `prompt`; fetches stayed pending until the harness aborted at 1.8 s, WebSockets timed out, and the local fixture saw no requests. WebKit 27.2 reports the same permission states but sends no local HTTP or `ws://` request; console says “Not allowed to request resource.” Its `wss://localhost:4843` probe did reach the fixture and echoed one byte without a visible prompt. Headless runs cannot show browser-chrome permission UI, so these are permission states and observed outcomes, not visual confirmation of a dialog. Evidence: `research/canvas-node/results/browser-probes.json`.
Author
Owner

Passkey finding (Google Chrome Stable 155.0.8059.39, CDP virtual authenticator): registration and assertion on the server RP ID example.com succeeded; a localhost page could register its own localhost RP ID but could not assert for example.com, even when the RP well-known JSON listed the localhost origins; a separate HTTPS origin at node.other.net succeeded for example.com when listed by the RP. This supports HTTPS per-node names as a browser-compatible Related Origin Request path, while localhost is not a valid related origin in this test. Evidence: research/canvas-node/results/browser-probes.json (webAuthn).

Passkey finding (Google Chrome Stable 155.0.8059.39, CDP virtual authenticator): registration and assertion on the server RP ID `example.com` succeeded; a `localhost` page could register its own `localhost` RP ID but could not assert for `example.com`, even when the RP well-known JSON listed the localhost origins; a separate HTTPS origin at `node.other.net` succeeded for `example.com` when listed by the RP. This supports HTTPS per-node names as a browser-compatible Related Origin Request path, while localhost is not a valid related origin in this test. Evidence: `research/canvas-node/results/browser-probes.json` (`webAuthn`).
Author
Owner

Linux multi-user finding (Debian 13, kernel 6.12.111+deb13-amd64): a browser-style TCP connection from OS user nobody (UID 65534) to a test listener owned by kayg (UID 1001) produced a live client-side /proc/net/tcp row with UID 65534 and an accepted server-side row with UID 1001. The client tuple plus row UID identifies the connecting local user; the accepted socket alone does not. The harness output is research/canvas-node/results/local-user-uid.json.

Linux multi-user finding (Debian 13, kernel 6.12.111+deb13-amd64): a browser-style TCP connection from OS user `nobody` (UID 65534) to a test listener owned by `kayg` (UID 1001) produced a live client-side `/proc/net/tcp` row with UID 65534 and an accepted server-side row with UID 1001. The client tuple plus row UID identifies the connecting local user; the accepted socket alone does not. The harness output is `research/canvas-node/results/local-user-uid.json`.
Author
Owner

Chrome CORS/PNA finding (Google Chrome Stable 155.0.8059.39): after CDP granted the loopback/local permissions, the simple GETs and ws/wss echoes succeeded. A custom-header fetch did ordinary CORS OPTIONS preflight (Access-Control-Request-Headers: x-canvas-probe), but the server saw no Access-Control-Request-Private-Network; the request returned 200 both when the preflight response omitted Access-Control-Allow-Private-Network and when it included it. Exact Access-Control-Allow-Origin and ordinary CORS method/header responses were present. Evidence: research/canvas-node/results/browser-probes.json (corsPreflight, serverRequests).

Chrome CORS/PNA finding (Google Chrome Stable 155.0.8059.39): after CDP granted the loopback/local permissions, the simple GETs and `ws`/`wss` echoes succeeded. A custom-header fetch did ordinary CORS OPTIONS preflight (`Access-Control-Request-Headers: x-canvas-probe`), but the server saw no `Access-Control-Request-Private-Network`; the request returned 200 both when the preflight response omitted `Access-Control-Allow-Private-Network` and when it included it. Exact `Access-Control-Allow-Origin` and ordinary CORS method/header responses were present. Evidence: `research/canvas-node/results/browser-probes.json` (`corsPreflight`, `serverRequests`).
Author
Owner

Latency finding (infraternal-dev, Debian 13, kernel 6.12.111+deb13-amd64): 60 browser round trips of a 1-byte WebSocket echo measured localhost WS p50 0.8 ms / p95 1.8 ms / p99 5.5 ms. Across a veth pair in two temporary network namespaces with netem delay 75ms on each egress direction, WSS p50 was 151.1 ms / p95 151.4 ms / p99 157.4 ms. The benchmark ran inside systemd-run --user --scope -p CPUWeight=10000; load average was 12.88 / 21.79 / 26.21 at start and 11.67 / 21.24 / 25.99 at end. Evidence: research/canvas-node/results/latency.json; namespaces and veths were removed at exit.

Latency finding (infraternal-dev, Debian 13, kernel 6.12.111+deb13-amd64): 60 browser round trips of a 1-byte WebSocket echo measured localhost WS p50 0.8 ms / p95 1.8 ms / p99 5.5 ms. Across a veth pair in two temporary network namespaces with `netem delay 75ms` on each egress direction, WSS p50 was 151.1 ms / p95 151.4 ms / p99 157.4 ms. The benchmark ran inside `systemd-run --user --scope -p CPUWeight=10000`; load average was 12.88 / 21.79 / 26.21 at start and 11.67 / 21.24 / 25.99 at end. Evidence: `research/canvas-node/results/latency.json`; namespaces and veths were removed at exit.
Author
Owner

Mobile finding (documentation only; no iPhone/iPad device or real Safari automation): Apple TN3179 states that local-network operations from Safari do not require the OS Local Network privilege, so the native-app Local Network prompt is not evidence for or against Safari page access. Apple supports adding a site to the iPhone Home Screen as a web app, while the W3C secure-context rules do not treat an ordinary LAN IP over HTTP like localhost. Direct Safari/PWA access to a same-LAN node and its mesh address remain unverified; the established server-origin path is still the only measured mobile path. Evidence and links: research/canvas-node/README.md §5.

Mobile finding (documentation only; no iPhone/iPad device or real Safari automation): Apple TN3179 states that local-network operations from Safari do not require the OS Local Network privilege, so the native-app Local Network prompt is not evidence for or against Safari page access. Apple supports adding a site to the iPhone Home Screen as a web app, while the W3C secure-context rules do not treat an ordinary LAN IP over HTTP like localhost. Direct Safari/PWA access to a same-LAN node and its mesh address remain unverified; the established server-origin path is still the only measured mobile path. Evidence and links: `research/canvas-node/README.md` §5.
Author
Owner

Dependency step complete: pinned Playwright 1.64.0 in an AGPL-3.0-only research package. Commit 35576215ea9d36c36f4807cd390128c0f9e05064. Gate: bun install --frozen-lockfile — Checked 2 installs across 3 packages (no changes) [539.00ms].

Dependency step complete: pinned Playwright 1.64.0 in an AGPL-3.0-only research package. Commit `35576215ea9d36c36f4807cd390128c0f9e05064`. Gate: `bun install --frozen-lockfile` — `Checked 2 installs across 3 packages (no changes) [539.00ms]`.
Author
Owner

Browser-probe step complete: recorded Chrome 155.0.8059.39, Firefox 157.0, and Playwright WebKit 27.2 across both localhost names; Chrome’s custom-header preflight succeeded without Access-Control-Allow-Private-Network. Evidence is in results/browser-probes.json. Commit 3c17fa9ce27754f6f1cbb6c78e0d25d0a3a566bb; git diff --cached --check passed with no output.

Browser-probe step complete: recorded Chrome 155.0.8059.39, Firefox 157.0, and Playwright WebKit 27.2 across both localhost names; Chrome’s custom-header preflight succeeded without `Access-Control-Allow-Private-Network`. Evidence is in `results/browser-probes.json`. Commit `3c17fa9ce27754f6f1cbb6c78e0d25d0a3a566bb`; `git diff --cached --check` passed with no output.
Author
Owner

Linux local-user step complete: UID 65534 (nobody) connected to a UID 1001 (kayg) listener. The client /proc/net/tcp row showed UID 65534; the accepted row showed UID 1001. Commit 60468d7ac60125574fc1b3afa4c5df858e60fa8c; evidence: results/local-user-uid.json.

Linux local-user step complete: UID 65534 (`nobody`) connected to a UID 1001 (`kayg`) listener. The client `/proc/net/tcp` row showed UID 65534; the accepted row showed UID 1001. Commit `60468d7ac60125574fc1b3afa4c5df858e60fa8c`; evidence: `results/local-user-uid.json`.
Author
Owner

Latency step complete: 60 one-byte round trips per path under the requested systemd CPU weight and two temporary network namespaces. Local WS p50/p95/p99: 0.8/1.8/5.5 ms; WSS over 75 ms netem per direction: 151.1/151.4/157.4 ms. Load average start/end: 12.88/21.79/26.21 and 11.67/21.24/25.99. Commit eb7303c01aed0c9b18fc8327317d1462680a5d1b; evidence: results/latency.json.

Latency step complete: 60 one-byte round trips per path under the requested systemd CPU weight and two temporary network namespaces. Local WS p50/p95/p99: 0.8/1.8/5.5 ms; WSS over 75 ms netem per direction: 151.1/151.4/157.4 ms. Load average start/end: 12.88/21.79/26.21 and 11.67/21.24/25.99. Commit `eb7303c01aed0c9b18fc8327317d1462680a5d1b`; evidence: `results/latency.json`.
Author
Owner

Continuing after the host resize on research-canvas-node, based on origin/dev at ded3abd609e5290a7027b6888ecd9864a2d30374; checkpoint head before restore was 019a4a6c. Ran the requested soft reset; restored checkpoint changes are staged on current head f4cef80c. Reviewing the owner’s per-node HTTPS extension and will split/push the restored work atomically.

Continuing after the host resize on `research-canvas-node`, based on `origin/dev` at `ded3abd609e5290a7027b6888ecd9864a2d30374`; checkpoint head before restore was `019a4a6c`. Ran the requested soft reset; restored checkpoint changes are staged on current head `f4cef80c`. Reviewing the owner’s per-node HTTPS extension and will split/push the restored work atomically.
Author
Owner

Handoff finding (Debian 13, Linux 6.12.111+deb13-amd64; Playwright 1.64.0; Chrome 155.0.8059.39, Firefox 157.0, WebKit 27.2; headless): popup postMessage with exact origin/source/state checks and top-level redirect back to the node both completed on http://localhost:4840 and http://127.0.0.1:4840 in all three engines. The fixture redeemed once (204), rejected replay (409), and a same-origin HttpOnly; Secure; SameSite=Strict cookie authenticated in Chrome and Firefox. WebKit did not store/send that Secure cookie on either HTTP loopback origin; an HttpOnly SameSite=Strict cookie without Secure worked there, and the Secure cookie worked over HTTPS localhost in all three. Redirect-code URLs were redacted from captured request paths. This is browser-mechanics evidence only; the code issuer and authorization were fixtures. Evidence: research/canvas-node/results/browser-probes.json (handoff, 21 probes), harness: research/canvas-node/browser-probes.mjs.

Handoff finding (Debian 13, Linux 6.12.111+deb13-amd64; Playwright 1.64.0; Chrome 155.0.8059.39, Firefox 157.0, WebKit 27.2; headless): popup `postMessage` with exact origin/source/state checks and top-level redirect back to the node both completed on `http://localhost:4840` and `http://127.0.0.1:4840` in all three engines. The fixture redeemed once (204), rejected replay (409), and a same-origin `HttpOnly; Secure; SameSite=Strict` cookie authenticated in Chrome and Firefox. WebKit did not store/send that Secure cookie on either HTTP loopback origin; an HttpOnly SameSite=Strict cookie without Secure worked there, and the Secure cookie worked over HTTPS localhost in all three. Redirect-code URLs were redacted from captured request paths. This is browser-mechanics evidence only; the code issuer and authorization were fixtures. Evidence: `research/canvas-node/results/browser-probes.json` (`handoff`, 21 probes), harness: `research/canvas-node/browser-probes.mjs`.
Author
Owner

Per-node HTTPS extension finding: Chromium 155’s CDP virtual authenticator asserted for RP ID example.com from https://node.other.net:4844 when that exact origin was listed by the RP’s /.well-known/webauthn; http://localhost:4840 failed even when listed. The fixture mapped node.other.net to the namespace test address and used a throwaway CA certificate with a browser pin, so this proves origin/ROR mechanics only, not production DNS, certificate trust, mesh reachability, or iOS behavior. Evidence: research/canvas-node/results/browser-probes.json (webAuthn). Resolver behavior is still environment-dependent; for example, OpenWrt documents dnsmasq rebind protection and its possible-rebind rejection log: https://openwrt.org/docs/guide-user/base-system/dhcp.dnsmasq. I’ll record the unmeasured resolver and signed-bundle compatibility costs in the report.

Per-node HTTPS extension finding: Chromium 155’s CDP virtual authenticator asserted for RP ID `example.com` from `https://node.other.net:4844` when that exact origin was listed by the RP’s `/.well-known/webauthn`; `http://localhost:4840` failed even when listed. The fixture mapped `node.other.net` to the namespace test address and used a throwaway CA certificate with a browser pin, so this proves origin/ROR mechanics only, not production DNS, certificate trust, mesh reachability, or iOS behavior. Evidence: `research/canvas-node/results/browser-probes.json` (`webAuthn`). Resolver behavior is still environment-dependent; for example, OpenWrt documents dnsmasq rebind protection and its possible-rebind rejection log: https://openwrt.org/docs/guide-user/base-system/dhcp.dnsmasq. I’ll record the unmeasured resolver and signed-bundle compatibility costs in the report.
Author
Owner

Latency harness finding: two post-resize attempts failed only on remote WSS. CDP showed local-network and loopback grants; the local WS leg passed. Diagnostic output showed Chrome net::ERR_INTERNET_DISCONNECTED and client veth-client LOWERLAYERDOWN; the script started the far-side namespace server asynchronously without waiting for its listener. Added a bounded listener-ready wait in run-latency-netns.sh. Rerun succeeded: 60 one-byte RTTs, local p50/p95/p99 1.2/3.1/10.0 ms; netem WSS 151.6/170.6/191.1 ms. Load average start/end 37.93/42.57/30.52 and 35.94/41.91/30.50 on the 12-vCPU host. Evidence: research/canvas-node/results/latency.json.

Latency harness finding: two post-resize attempts failed only on remote WSS. CDP showed local-network and loopback grants; the local WS leg passed. Diagnostic output showed Chrome `net::ERR_INTERNET_DISCONNECTED` and client `veth-client` `LOWERLAYERDOWN`; the script started the far-side namespace server asynchronously without waiting for its listener. Added a bounded listener-ready wait in `run-latency-netns.sh`. Rerun succeeded: 60 one-byte RTTs, local p50/p95/p99 1.2/3.1/10.0 ms; netem WSS 151.6/170.6/191.1 ms. Load average start/end 37.93/42.57/30.52 and 35.94/41.91/30.50 on the 12-vCPU host. Evidence: `research/canvas-node/results/latency.json`.
Author
Owner

Browser handoff step committed and pushed: 95540f6c. The harness records 21 popup, redirect, and cookie probes across Chrome 155.0.8059.39, Firefox 157.0, and WebKit 27.2 on localhost, 127.0.0.1, and HTTPS localhost. Popup and redirect code redemption passed in all three; Secure cookies on HTTP loopback failed only in WebKit. node --check and git diff --check passed; browser run exited 0. Evidence: research/canvas-node/results/browser-probes.json.

Browser handoff step committed and pushed: `95540f6c`. The harness records 21 popup, redirect, and cookie probes across Chrome 155.0.8059.39, Firefox 157.0, and WebKit 27.2 on localhost, 127.0.0.1, and HTTPS localhost. Popup and redirect code redemption passed in all three; Secure cookies on HTTP loopback failed only in WebKit. `node --check` and `git diff --check` passed; browser run exited 0. Evidence: `research/canvas-node/results/browser-probes.json`.
Author
Owner

Latency step committed and pushed: 45723a8e. The two-namespace harness now waits for the remote WSS listener; a 60-sample rerun passed at local p50/p95/p99 1.2/3.1/10.0 ms and netem WSS 151.6/170.6/191.1 ms. Load averages were 37.93/42.57/30.52 at start and 35.94/41.91/30.50 at end. bash -n, node --check, and git diff --check passed. Evidence: research/canvas-node/results/latency.json; initial run retained in latency-first-run.json.

Latency step committed and pushed: `45723a8e`. The two-namespace harness now waits for the remote WSS listener; a 60-sample rerun passed at local p50/p95/p99 1.2/3.1/10.0 ms and netem WSS 151.6/170.6/191.1 ms. Load averages were 37.93/42.57/30.52 at start and 35.94/41.91/30.50 at end. `bash -n`, `node --check`, and `git diff --check` passed. Evidence: `research/canvas-node/results/latency.json`; initial run retained in `latency-first-run.json`.
Author
Owner

Finished and pushed research-canvas-node at 2202eaf996f32c167a7c243572bfc0392ea7c447; local and remote heads match and the worktree is clean. Browser handoff, latency, and report changes are split into commits 95540f6c, 45723a8e, and 2202eaf9.

Final gate output:

git diff --check
(no output; exit 0)

git diff --check origin/dev...HEAD also produced no output and exited 0. The research brief does not require scripts/verify fast. Report and evidence: research/canvas-node/README.md and research/canvas-node/results/.

Finished and pushed `research-canvas-node` at `2202eaf996f32c167a7c243572bfc0392ea7c447`; local and remote heads match and the worktree is clean. Browser handoff, latency, and report changes are split into commits `95540f6c`, `45723a8e`, and `2202eaf9`. Final gate output: ``` git diff --check (no output; exit 0) ``` `git diff --check origin/dev...HEAD` also produced no output and exited 0. The research brief does not require `scripts/verify fast`. Report and evidence: `research/canvas-node/README.md` and `research/canvas-node/results/`.
Author
Owner

Closed by the orchestrator during the 2026-10-28 handover (#254), on the owner's request to close finished issues. Evidence: Research delivered (research/canvas-node, 2202eaf9) and owner decisions recorded as D248. Reopen if anything is still missing.

Closed by the orchestrator during the 2026-10-28 handover (#254), on the owner's request to close finished issues. Evidence: Research delivered (research/canvas-node, 2202eaf9) and owner decisions recorded as D248. Reopen if anything is still missing.
kayg closed this issue 2026-10-28 17:52:59 +00:00
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#167
No description provided.