Skip to content

feat(M4): FPP-forward farbling + dark mode unlock - #20

Merged
neon798 merged 10 commits into
mainfrom
feat/m4-fpp-farbling
Jul 13, 2026
Merged

neon798 merged 10 commits into
mainfrom
feat/m4-fpp-farbling

Conversation

@neon798

@neon798 neon798 commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Switches anti-fingerprinting from RFP (uniform, detectable, per-read canvas randomization) to FPP (per-origin deterministic farbling) — the roadmap's 'convincing common fingerprint' posture. The FP modes are mutually exclusive, so RFP goes off.

Also unlocks dark mode (a stated adoption blocker): RFP was forcing prefers-color-scheme: light; under FPP the native 'Website appearance' setting works.

Verified locally (Marionette, no rebuild — runtime/startup prefs)

  • Canvas farbling: deterministic per-origin (two same-origin reads identical) ✓
  • hardwareConcurrency pinned to 4 ✓
  • Dark mode unlocked: force-dark honored under FPP; ignored (forced light) under RFP ✓
  • Timezone: intentionally left real (JS override is RFP-pref-gated; mismatched TZ is a tell) — documented

Merge gate (NOT done yet — needs headful GL + network)

browserleaks canvas/webgl/audio/fonts (site-varying, protected) + coveryourtracks (randomized, no privacy-browser tell). WebGL can't be tested headless (no GL context).

🤖 Generated with Claude Code

neon798 and others added 5 commits July 7, 2026 20:45
Switch anti-fingerprinting from RFP (uniform, "stands out", per-read canvas
randomization) to FPP (per-origin deterministic farbling), the roadmap's
"convincing common fingerprint" posture. The three FP modes are mutually
exclusive, so RFP must be off for FPP to run.

assets/neonwolf.overrides.cfg:
- privacy.resistFingerprinting=false, privacy.fingerprintingProtection=true,
  remoteOverrides.enabled=false.
- fingerprintingProtection.overrides adds WebGL randomization, hw-concurrency
  pin, audio/font/timer protection on top of FPP's default canvas farbling.
  UA/screen uniform spoofs deliberately omitted (real-for-platform is more
  common); timezone left real (its JS override is RFP-pref-gated, and a
  timezone/IP mismatch is itself a tell).
- webgl.disabled=false so WebGL is farbled (WebGL-off is a conspicuous tell).
- layout.css.prefers-color-scheme.content-override=2 (auto).

Verified locally (Marionette): canvas farbling deterministic per-origin
(Brave-style), hardwareConcurrency pinned to 4, and DARK MODE unlocked
(prefers-color-scheme honors the override under FPP; RFP was forcing light).

NOT merged: per plan, ships only behind a full browserleaks/coveryourtracks +
WebGL pass (needs a real GL context + network — headful), proving it's more
convincing, not less protected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AI cowork.md (the Strong/Weak-AI playbook), GROK.md (the handoff artifact it
prescribes), and docs/agents/ are local working scratch, never repo content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Empirical validation of the FPP posture found WebGL still dead in content:
LibreWolf's webgl-permission.patch gates context creation behind a per-site
'webgl' permission (librewolf.webgl.prompt, compiled default TRUE) and ships
the doorhanger hidden — silent deny everywhere, the exact same conspicuous
tell webgl.disabled=false was meant to remove. Turn the gate off; WebGL now
runs farbled (+WebGLRandomization per-eTLD+1 readback noise, verified
deterministic per-origin/per-session; +WebGLRenderInfo spoofs Mozilla/Mozilla).

Full headful validation (Marionette on Wayland, real network): canvas +
WebGL farble per-eTLD+1, stable across reloads, re-keyed per session, plain
control identical across domains with all tiers off; hwConcurrency pinned 4;
timezone/screen/UA real; dark mode honored; CoverYourTracks verdict 'strong
protection against Web tracking'. Known gap: audio readback is NOT farbled
(FPP has no audio randomization target — RFP-only protection; follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scriptions UI

Shields panel (M4 'integrate with Shields'):
- fingerprinting toggle now drives privacy.fingerprintingProtection (the M4
  posture pref), label 'Farble fingerprinting'
- new per-site 'Farble on this site' subrow writing an -AllTargets entry to
  BOTH privacy.fingerprintingProtection.granularOverrides AND the baseline
  tier's granularOverrides (canvas farbling rides baseline FPP — verified
  empirically); foreign/webcompat entries are never touched, unreadable pref
  hides the row (all-or-nothing writes), PSL-hosted eTLD+1 edge documented

Element picker (M5): real selector generator replacing the placeholder —
unique #id fast path, bottom-up max-4-segment path with stable-class
filtering (hashed CSS-module classes rejected), :nth-of-type disambiguation,
document-uniqueness testing, simpleSelector fallback; pure DOM reads only.

Lists page (M5): 'Subscribed lists' manager bound to
neonwolf.shields.lists.subscriptions — https-only validation, dedupe, live
pref observer, Remove per row, synthwave-styled via shared filters classes.

Implemented by Grok (executor) from Claude's spec; line-by-line reviewed,
staged into the built tree and Marionette-verified end-to-end (granular-pref
roundtrip incl. corrupt-pref no-clobber, page render screenshot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UBONetFilter now compiles neonwolf.shields.lists.subscriptions (newline-
separated https:// URLs, managed by the Shields lists/filters pages) as extra
engine lists on every live refresh, and a pref observer triggers an immediate
refresh on subscribe/unsubscribe. Fetch-only by design: no bundled fallback
(a failed fetch drops that subscription until the next cycle), 1-byte floor
(personal lists can be tiny) but a 20 MB cap so a hostile URL can't balloon
the compile; invalid/non-https lines are ignored. No selfie write — like the
bundled-list refresh, subscriptions apply in-memory after the first refresh
of a session.

Marionette-verified end-to-end against the built tree: subscribe to
easylistgermany -> refresh fires -> adnx.de script blocked; unsubscribe ->
refresh -> unblocked again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@neon798

neon798 commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

M4 merge gate: PASSED (overnight validation run, headful Marionette on Wayland, real network, built tree)

Check Result
Canvas noise deterministic per eTLD+1 (subdomains match, reload stable) ✅
Canvas differs across eTLD+1s, re-keys per session; true-plain control identical across domains ✅
WebGL live + farbled per-origin (was silently denied by LibreWolf's librewolf.webgl.prompt gate — fixed in 3119d1b) ✅
hardwareConcurrency pinned 4 (real 6); tz/screen/UA real ✅
Dark mode honored in content ✅
CoverYourTracks "strong protection against Web tracking", ads + invisible trackers blocked
Shields per-site farbling exempt (writes FPP and baseline granular overrides — canvas rides the baseline tier) ✅
Subscriptions backend end-to-end (subscribe → blocks, unsubscribe → unblocks) ✅
make check-patchfail ✅

Known gap for a follow-up: audio readback is not farbled (FPP has no audio noise target; that protection was RFP-only).

🤖 Generated with Claude Code

neon798 and others added 2 commits July 9, 2026 07:21
…k gap

First live adblock benchmark (adblock.turtlecute.org, the maintained d3ward
replacement — d3ward is archived) scored 116/133. Engine-side probing of every
test host pinned the shortfall to 9 tracker/ad network hosts unmatched by all
ten bundled lists: TikTok ads/events APIs, Yahoo ad-tech + DMP, and the
Yandex Metrika/AppMetrica apex (bundled lists only covered paths/subdomains).

Add a curated assets/adblock/neonwolf-extra.txt (bundled-only, no fetch URL —
served from the dump) with those 9 hosts as $third-party rules so first-party
use of each console still works. Wired into gen-adblock-dump.py's LISTS, the
list_names pref, and the lists-page display.

Re-benchmarked against the rebuilt engine: 125/133 = 94%, all 131 network
hosts now blocked (verified via direct matchRequest). The remaining 8 are
cosmetic-filter + ad-script-loading checks — the known M2 scriptlet gap
(scriptlets pref-disabled pending the document_start race fix), not a list gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the archived d3ward metric with adblock.turtlecute.org throughout;
record M4 (FPP-forward farbling, validated PR #20) and M5 (picker, custom
filters, subscriptions) as functionally complete with 2026-07-08 progress notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@neon798

neon798 commented Jul 9, 2026

Copy link
Copy Markdown
Owner Author

Follow-up shipped: adblock benchmark gap closed → 94%.

d3ward is archived; benchmarked on adblock.turtlecute.org (the maintained d3ward-style replacement). First run was 116/133 (87%). Engine-side probing pinned the shortfall to 9 tracker/ad hosts unmatched by all ten bundled lists (TikTok ads/events APIs, Yahoo ad-tech + DMP, Yandex Metrika/AppMetrica apex).

Added a curated bundled list assets/adblock/neonwolf-extra.txt (those 9 hosts as $third-party), wired into the dump generator + list_names + the lists page. Re-benchmarked against the rebuilt engine: 125/133 = 94%, all 131 network hosts blocked (verified via direct matchRequest). The remaining 8 are cosmetic-filter + ad-script-loading checks = the known M2 scriptlet gap (scriptlets pref-disabled pending the document_start race fix), not a list gap — so 94% is the network-list ceiling until scriptlets land.

Also refreshed PLAN_OF_ACTION.md (metric d3ward→turtlecute; M4/M5 marked shipped). make check-patchfail still green.

🤖 Generated with Claude Code

neon798 and others added 3 commits July 9, 2026 21:29
Complete the Google Safe Browsing opt-out for this privacy-focused build. The
LibreWolf baseline already disables the SB malware/phishing/blockedURIs/downloads
features and blanks the google4 + legacy-google gethash/update URLs, but leaves
the newer google5 provider (stock FF152 ships it enabled) and the google4
dataSharing toggle untouched — a residual safebrowsing.googleapis.com phone-home.
Turn those off too so no Google SB lookup remains; malware/phishing coverage
comes from the bundled uBO lists (ubo-badware et al.) via the native engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The notifier queried GitHub /releases/latest, which excludes prereleases and
drafts. Beta builds ship as GitHub *prereleases*, so a beta tester would never
be told a newer beta exists. Query /releases?per_page=30 instead and scan for
the highest-version non-draft release (prereleases eligible, drafts skipped),
reusing the existing _isNewer comparator and keeping the throttle, enable gate,
and error handling unchanged.

Verified against the live GitHub API: scans 5 releases, correctly picks
152.0.1-3, treats the 152.0.1-1 prerelease as eligible.

Executor: Grok (worktree, no-commit); reviewed, tested, and captured by Claude.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ection

Public-facing beta docs for Linux + Windows testers: download links, how to
install the unsigned builds past SmartScreen / run the AppImage, how the
notify-only update check works (no auto-update by design), and a Known Issues
list (adblock ~94% pending scriptlets, audio not farbled, no macOS build,
unsigned, no built-in password manager, Windows less-tested). README gains a
short Beta downloads section linking it.

Executor: Grok (worktree, no-commit, facts-only spec); reviewed line-by-line
against the known facts and captured by Claude.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@neon798
neon798 merged commit 56ed85a into main Jul 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant