Skip to content

Commit 007a075

Browse files
authored
Merge pull request #34 from reclaimprotocol/staging
feat: mobile and embedded live-view stack for minimal-vnc-desktop
2 parents e514fdb + 78d26fe commit 007a075

122 files changed

Lines changed: 21935 additions & 167 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

images/minimal-vnc-desktop/Dockerfile

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@ ARG UBUNTU_IMAGE=docker.io/ubuntu:22.04@sha256:962f6cadeae0ea6284001009daa4cc9a8
55
# C++ persona patches, distributed as an OCI image. It fully replaces stock
66
# chromium (the xtradeb chromium debs are not installed); the chromium
77
# runtime-library closure it links against is supplied by the pinned apt
8-
# packages in locks/apt-packages.txt. Pinned by digest to the STABLE channel —
9-
# tag "149" (Chromium 149.0.7827.232), which blends in with the mass of real
10-
# Chrome users, NOT the newer :151 "latest" build. **amd64-only** (no arm64
11-
# manifest), so this whole image is amd64-only: build on linux/amd64. Chromium
12-
# SIGTRAPs under QEMU, so it must build & run on a native amd64 host.
13-
ARG FORTRESS_IMAGE=docker.io/tilion/fortress@sha256:a5e31e67b53c11f5992fb74e4acc4c6c5313cc960def5c5898d198713a58b72a
8+
# packages in locks/apt-packages.txt.
9+
#
10+
# Pinned by digest to the STABLE channel (150). **amd64-only** — Fortress
11+
# publishes no arm64 manifest, so this whole image is amd64-only: build and run
12+
# it on a native amd64 host, since Chromium SIGTRAPs under QEMU emulation.
13+
# To confirm the version in a built image read /json/version over CDP; do NOT
14+
# trust /opt/*/README.txt, whose banner carries a stale generic string.
15+
#
16+
# 150 is built from CURRENT STABLE Chrome, which matters on its own: version-database
17+
# detectors read an older build as out-of-date, and a version that does not publicly
18+
# exist as generic "Chromium". This release also carries the fixes from upstream's
19+
# cycle (tiliondev/fortress#25 and the 150 notes):
20+
# - per-clone canvas/audio divergence — the noise seed is applied correctly, so two
21+
# instances no longer share one un-noised fingerprint
22+
# - explicit --uxr-* flags now WIN over the generated persona (previously --uxr-ua-os /
23+
# --uxr-ua-platform were overwritten, so a UA override silently stayed Windows and
24+
# the resulting UA-vs-platform mismatch read as tampering)
25+
# - the launcher honours the documented TILION_* env names, not only TILLION_*
26+
# - geo-matched persona: timezone/locale derived from the egress IP, since a timezone
27+
# disagreeing with the IP reads as VPN
28+
# - coherent OS voices, and the completed WebGL parameter spoof under software render
29+
ARG FORTRESS_IMAGE=docker.io/tilion/fortress@sha256:b4f331693d3db25d70d814a99ed0da6be433d884e18d5af1cf3e6f3a2969569b
1430

1531
FROM ${GOLANG_IMAGE} AS proxy-builder
1632
WORKDIR /src
@@ -104,7 +120,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap
104120
novnc_url="$(awk -F '=' '$1 == "url" { print $2 }' /tmp/novnc.lock)"; \
105121
novnc_sha256="$(awk -F '=' '$1 == "sha256" { print $2 }' /tmp/novnc.lock)"; \
106122
mkdir -p /tmp/novnc-src /usr/share/novnc; \
107-
curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --output /tmp/novnc.tar.gz "$novnc_url"; \
123+
curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 \
124+
--connect-timeout 15 --speed-limit 4096 --speed-time 20 --max-time 300 \
125+
--ipv4 --output /tmp/novnc.tar.gz "$novnc_url"; \
108126
echo "${novnc_sha256} /tmp/novnc.tar.gz" | sha256sum -c -; \
109127
tar -xzf /tmp/novnc.tar.gz --strip-components=1 -C /tmp/novnc-src; \
110128
cp -a /tmp/novnc-src/core /tmp/novnc-src/vendor /usr/share/novnc/; \
@@ -144,17 +162,24 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap
144162
-newerct "$touch_start" \
145163
-exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
146164

147-
# Tilion Fortress (stable stealth Chromium 149) — the only browser in the image.
165+
# Tillion Fortress (stable stealth Chromium 150) — the only browser in the image.
148166
# /opt/tilion/tilion is a bash launcher that applies a Windows persona + bundled
149167
# fonts + its own SwiftShader ICD around /opt/tilion/chrome (the patched Chromium
150168
# whose C++-level patches produce a real-Chrome TLS/JS fingerprint). The shared
151169
# libs it links against come from the chromium runtime-lib block in
152170
# locks/apt-packages.txt. `chromium`/`chromium-browser` are symlinked to the
153171
# launcher so anything invoking `chromium` gets the persona-wrapped browser.
154172
# Own layer so the ~326MB copy caches independently.
155-
COPY --from=fortress /opt/tilion /opt/tilion
173+
# Copy /opt wholesale rather than a fixed subdirectory: the 151 image renamed
174+
# /opt/tilion/tilion to /opt/tillion/tillion (double L). Normalising to a stable
175+
# /opt/tilion/tilion below means the rest of the image — the symlinks, start-chromium's
176+
# CHROME_BIN, the fonts template patch — is unaffected by upstream renaming, and the
177+
# build fails loudly rather than silently launching nothing if the layout changes again.
178+
COPY --from=fortress /opt /opt
156179
RUN set -eux; \
157180
touch_start="$(date -u +'%Y-%m-%d %H:%M:%S')"; \
181+
if [ -d /opt/tillion ] && [ ! -e /opt/tilion ]; then ln -s tillion /opt/tilion; fi; \
182+
if [ -x /opt/tilion/tillion ] && [ ! -e /opt/tilion/tilion ]; then ln -s tillion /opt/tilion/tilion; fi; \
158183
test -x /opt/tilion/tilion; test -x /opt/tilion/chrome; \
159184
ln -sf /opt/tilion/tilion /usr/bin/chromium; \
160185
ln -sf /opt/tilion/tilion /usr/bin/chromium-browser; \
@@ -217,6 +242,49 @@ COPY --from=proxy-builder /out/novnc-proxy /usr/local/bin/novnc-proxy
217242
COPY --chmod=0755 entrypoint.sh /usr/local/bin/minimal-vnc-entrypoint
218243
COPY --chmod=0755 start-chromium /usr/local/bin/start-chromium
219244
COPY liveview.html /usr/share/novnc/liveview.html
245+
COPY kbd-autofocus.js /usr/share/novnc/kbd-autofocus.js
246+
COPY kbd /usr/share/novnc/kbd
247+
COPY viewer.js /usr/share/novnc/viewer.js
248+
COPY locks/esbuild.lock /tmp/esbuild.lock
249+
250+
# Bundle the viewer (noVNC core/* + the kbd/* IME layer + viewer.js) into a single
251+
# viewer.bundle.js so the page loads in ONE request instead of a ~70-module ES
252+
# graph that the browser discovers level-by-level over the tunnel (measured
253+
# multi-second cold start on 3G). esbuild is a standalone Go binary — no Node —
254+
# pinned by sha256 exactly like noVNC. --format=esm keeps module semantics.
255+
# --minify-whitespace/--minify-syntax strip whitespace + fold dead code (~-22% on
256+
# the wire after gzip) WITHOUT mangling identifiers, so function names stay in
257+
# stack traces and the BUILD_TAG string stays greppable — the bundle stays
258+
# debuggable. (Full --minify would mangle names; deliberately not used.) esbuild's
259+
# output is deterministic (no embedded timestamps), preserving reproducibility.
260+
RUN set -eux; \
261+
esb_url="$(awk -F '=' '$1 == "url" { print $2 }' /tmp/esbuild.lock)"; \
262+
esb_sha256="$(awk -F '=' '$1 == "sha256" { print $2 }' /tmp/esbuild.lock)"; \
263+
curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 \
264+
--connect-timeout 15 --speed-limit 4096 --speed-time 20 --max-time 300 \
265+
--ipv4 --output /tmp/esbuild.tgz "$esb_url"; \
266+
echo "${esb_sha256} /tmp/esbuild.tgz" | sha256sum -c -; \
267+
mkdir -p /tmp/esbuild-src; \
268+
tar -xzf /tmp/esbuild.tgz --strip-components=1 -C /tmp/esbuild-src package/bin/esbuild; \
269+
/tmp/esbuild-src/bin/esbuild /usr/share/novnc/viewer.js --bundle --format=esm \
270+
--minify-whitespace --minify-syntax \
271+
--outfile=/usr/share/novnc/viewer.bundle.js; \
272+
rm -rf /tmp/esbuild.tgz /tmp/esbuild-src /tmp/esbuild.lock; \
273+
# Content-hash the bundle so novnc-proxy can serve it immutably (max-age=1y):
274+
# a content change yields a NEW url, so a reconnect/reopen skips re-downloading
275+
# it entirely — with zero staleness risk (the reason liveview.html itself stays
276+
# no-store, pointing at whatever hash is current). Deterministic: the hash is of
277+
# esbuild's deterministic output. Rewrite the <script src> to the hashed name.
278+
bundle_hash="$(sha256sum /usr/share/novnc/viewer.bundle.js | cut -c1-12)"; \
279+
mv /usr/share/novnc/viewer.bundle.js "/usr/share/novnc/viewer-${bundle_hash}.bundle.js"; \
280+
sed -i "s#src=\"viewer\.bundle\.js\"#src=\"viewer-${bundle_hash}.bundle.js\"#" \
281+
/usr/share/novnc/liveview.html; \
282+
# Precompress the (hashed) bundle: it's the cold-start bottleneck over the
283+
# tunnel and novnc-proxy serves the .gz via Accept-Encoding negotiation. -n
284+
# drops the name/mtime from the gzip header so the output is deterministic;
285+
# -k keeps the raw file for clients without gzip.
286+
gzip -9 -n -k -f "/usr/share/novnc/viewer-${bundle_hash}.bundle.js"
287+
220288
COPY third-party/fortress /usr/share/doc/popcorn/third-party/fortress
221289
COPY --chown=kernel:kernel extensions/proxy /home/kernel/extensions/proxy
222290
COPY --chown=kernel:kernel policies/managed/policy.json /etc/chromium/policies/managed/policy.json
@@ -228,8 +296,12 @@ RUN set -eux; \
228296
/usr/local/bin/novnc-proxy \
229297
/usr/local/bin/minimal-vnc-entrypoint \
230298
/usr/local/bin/start-chromium \
231-
/usr/share/novnc/liveview.html; \
232-
find /home/kernel/extensions /etc/chromium/policies /etc/chromium/policy-variants /usr/share/doc/popcorn -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
299+
/usr/share/novnc/liveview.html \
300+
/usr/share/novnc/kbd-autofocus.js \
301+
/usr/share/novnc/viewer.js \
302+
/usr/share/novnc/viewer-*.bundle.js \
303+
/usr/share/novnc/viewer-*.bundle.js.gz; \
304+
find /usr/share/novnc/kbd /home/kernel/extensions /etc/chromium/policies /etc/chromium/policy-variants /usr/share/doc/popcorn -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
233305

234306
USER kernel
235307
EXPOSE 6080 9222 9226

images/minimal-vnc-desktop/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ Open:
7070
http://localhost:6080/liveview.html?resize=scale&autoconnect=1
7171
```
7272

73+
To exercise the **embedded** viewer instead (a host page iframing the live view and
74+
feeding it keyboard geometry), serve this directory on another port and open one of
75+
the harnesses in `host/`:
76+
77+
```bash
78+
python3 -m http.server 8080 # from images/minimal-vnc-desktop
79+
# then: http://localhost:8080/host/test-min.html (bare iframe)
80+
# http://localhost:8080/host/test-host.html (+ debug panel, buttons, ?nest=1)
81+
```
82+
7383
noVNC HTTP/WebSocket is served on `6080`. Restricted CDP is served on `9222`
7484
and full CDP is served on `9226` for trusted internal routing. Raw VNC listens
7585
on `127.0.0.1:5900` inside the container, and Chromium's raw DevTools endpoint

images/minimal-vnc-desktop/STEALTH.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,12 @@ proxy (9222) filters the Runtime/Page commands the probes need. See
110110

111111
- **IP reputation matters.** Fortress improves browser coherence but cannot
112112
compensate for an exit address with poor reputation.
113-
- **Incoherent GPU persona (stable-149).** The default persona claims an RTX 3060
114-
(`--uxr-webgl-renderer="ANGLE (NVIDIA … RTX 3060 …, D3D11)"`) while rendering
115-
through SwiftShader — the hardware-GPU-on-software-renderer tell strict
116-
detectors (DataDome) cross-check. For strict flows override the GPU `--uxr`
117-
flags via `CHROMIUM_FLAGS` to match SwiftShader, or launch
118-
`TILION_NO_DEFAULTS=1` with a hand-built coherent persona.
113+
- **Software graphics backend.** The image renders through SwiftShader. The
114+
default `GPU_PERSONA=intel` reports a common Intel integrated GPU because it
115+
clears the visible Sannysoft SwiftShader row without lowering the current
116+
CreepJS score. Set `GPU_PERSONA=swiftshader` when strict renderer honesty is
117+
preferred over consumer-laptop plausibility, or `GPU_PERSONA=nvidia` to compare
118+
against Fortress's own default.
119119
- **Full CDP (9226) is internal-only.** It is an unauthenticated, fully
120120
controllable browser — never expose it publicly; never enter real credentials
121121
on a public tunnel.

images/minimal-vnc-desktop/build.sh

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@ PLATFORM="${PLATFORM:-}"
99
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "$REPO_ROOT" log -1 --pretty=%ct)}"
1010
UBUNTU_SNAPSHOT="${UBUNTU_SNAPSHOT:-$(awk -F '=' '$1 == "UBUNTU_SNAPSHOT" { print $2 }' "$SCRIPT_DIR/locks/ubuntu-snapshot.lock")}"
1111

12+
# Fortress (the browser engine) is amd64-only, so the image is ALWAYS amd64 —
13+
# an arm64 host builds it under emulation. Defaulting to the host arch here
14+
# silently produced an arm64 image that tunnel.sh (amd64-pinned) then couldn't
15+
# run. Pin amd64; override PLATFORM only if you truly know what you're doing.
1216
if [[ -z "$PLATFORM" ]]; then
13-
case "$(uname -m)" in
14-
x86_64|amd64) PLATFORM=linux/amd64 ;;
15-
arm64|aarch64) PLATFORM=linux/arm64 ;;
16-
*)
17-
echo "Unsupported host architecture: $(uname -m)" >&2
18-
exit 1
19-
;;
20-
esac
17+
PLATFORM=linux/amd64
2118
fi
2219

2320
"$SCRIPT_DIR/prepare-artifacts.sh" \

images/minimal-vnc-desktop/entrypoint.sh

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ DISPLAY_NUM="${DISPLAY_NUM:-1}"
55
WIDTH="${WIDTH:-1920}"
66
HEIGHT="${HEIGHT:-1080}"
77
DEPTH="${DEPTH:-24}"
8+
# Boot geometry for the X screen; start-chromium sizes the kiosk window to match.
9+
# Screen rows the window does not cover render as the bare X root (black), and
10+
# neither Chromium nor openbox resizes a --kiosk window after startup — so the
11+
# proxy keeps window == screen at the X level as viewers resize it
12+
# (proxy/window.go). Boot height is therefore a starting size, not a ceiling;
13+
# set FB_HEIGHT only to boot deliberately taller than the advertised desktop.
14+
FB_HEIGHT="${FB_HEIGHT:-$HEIGHT}"
15+
if (( FB_HEIGHT < HEIGHT )); then FB_HEIGHT="$HEIGHT"; fi
16+
export FB_HEIGHT # start-chromium sizes the kiosk window from it
17+
export WIDTH HEIGHT # novnc-proxy reads them for the default viewport emulation
818
VNC_PORT="${VNC_PORT:-5900}"
919
NOVNC_PORT="${NOVNC_PORT:-6080}"
1020
CDP_INTERNAL_PORT="${CDP_INTERNAL_PORT:-${CHROME_REMOTE_DEBUGGING_PORT:-9223}}"
@@ -231,20 +241,38 @@ setup_logging
231241
configure_agones
232242
start_agones_health
233243

234-
echo "[entrypoint] Starting Xvnc on ${DISPLAY} (${WIDTH}x${HEIGHT}x${DEPTH})"
244+
echo "[entrypoint] Starting Xvnc on ${DISPLAY} (${WIDTH}x${FB_HEIGHT}x${DEPTH}, desktop ${WIDTH}x${HEIGHT})"
245+
# -SendPrimary=0: do NOT forward the X PRIMARY selection to the client. PRIMARY is
246+
# claimed by merely SELECTING text, and TigerVNC forwards it by default
247+
# (SendPrimary=1) — where the viewer mirrors any incoming clipboard into the
248+
# DEVICE's real clipboard (kbd/clipboard.js onRemoteClipboard). So a plain Ctrl+A
249+
# in the remote page pushed the selection to the client with no copy at all;
250+
# verified with a canary string over raw RFB. Two consequences, both bad: remote
251+
# content lands in the user's OS clipboard unasked, and whatever they had copied
252+
# locally (a password they were about to paste) is destroyed — which reads as
253+
# "paste is broken".
254+
#
255+
# -SetPrimary=0 is the same story inbound: without it a client cut-text also
256+
# overwrites the remote PRIMARY. Only the real CLIPBOARD selection — a deliberate
257+
# Ctrl+C — should cross, in either direction.
235258
Xvnc "$DISPLAY" \
236-
-geometry "${WIDTH}x${HEIGHT}" \
259+
-geometry "${WIDTH}x${FB_HEIGHT}" \
237260
-depth "$DEPTH" \
238261
-rfbport "$VNC_PORT" \
239262
-localhost=1 \
240263
-SecurityTypes None \
241264
-AlwaysShared=1 \
265+
-SendPrimary=0 \
266+
-SetPrimary=0 \
242267
-Log '*:stderr:30' \
243268
> "$(log_file xvnc)" 2>&1 &
244269
pids+=("$!")
245270

246271
wait_for_tcp 127.0.0.1 "$VNC_PORT" Xvnc
247272

273+
vncconfig -nowin > "$(log_file vncconfig)" 2>&1 &
274+
pids+=("$!")
275+
248276
echo "[entrypoint] Starting noVNC/CDP proxy on :${NOVNC_PORT}, ${CDP_RESTRICTED_LISTEN}, ${CDP_FULL_LISTEN}"
249277
novnc-proxy \
250278
--listen "0.0.0.0:${NOVNC_PORT}" \
@@ -285,6 +313,9 @@ pids+=("$app_pid")
285313

286314
echo "[entrypoint] Waiting for app readiness pattern: ${READY_WINDOW_PATTERN}"
287315
wait_for_window "$READY_WINDOW_PATTERN" "$READY_TIMEOUT" "$app_pid"
316+
317+
# No post-boot resize here: the screen stays at WIDTH x FB_HEIGHT and viewers
318+
# drive it from there, with the window following (proxy/window.go).
288319
touch "$READY_FILE"
289320
echo "[entrypoint] noVNC is ready"
290321
agones_ready

0 commit comments

Comments
 (0)