Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Dugle

A Chromium fork whose device fingerprint is compiled into the binary and selected by a command-line flag. Give it a seed, and every profile launched with that seed is the same machine — same GPU, same core count, same screen, same canvas and audio readings — every time. Give it a different seed and it is a different machine. Nothing is injected into the page at runtime, so the spoofing survives Function.prototype.toString, property descriptor checks, iframes, and workers.

Built from ungoogled-chromium 150.0.7871.181 (revision 24b04c927b23c39cf9c5227cc8dc6f64a744c8e9, branch-heads/7871).

Dugle.app --fingerprint=42 --fingerprint-os=macos --user-data-dir=./profiles/alice

A Dugle window showing the bundled fingerprint report


Contents


Why a fork instead of a script

The usual way to change a fingerprint is to inject JavaScript that overrides getParameter, toDataURL, getChannelData, and friends. That works until a page asks a slightly different question:

Object.getOwnPropertyDescriptor(WebGLRenderingContext.prototype, 'getParameter')
CanvasRenderingContext2D.prototype.getImageData.toString()   // "function () { [native code] }" ?
new Worker(...)                                              // does the override exist in here too?
document.createElement('iframe').contentWindow.navigator     // and in a fresh realm?

Dugle answers those the same way a real browser does, because there is no override to find: the values are produced by the C++ that produces every other value. The seed reaches the renderer process on its own command line, and the patched code paths in Blink read it directly.

The trade-off is honest: a compiled fork can only spoof what its C++ touches. Everything else — the time zone, the exit IP, the User-Agent string — is the launcher's job, and this README is explicit about which is which.


Install

Download the archive for your platform from Releases and unpack it. Nothing installs; the folder is the browser.

file unpacked
macOS (Apple Silicon) Dugle-150.0.7871.181-macos-arm64.zip — 142 MB Dugle.app, 453 MB
Windows (x64) Dugle-150.0.7871.181-windows-x64.zip — 186 MB Dugle\, 570 MB

Verify the download against SHA256SUMS.txt published with the release:

shasum -a 256 -c SHA256SUMS.txt     # macOS / Linux
certutil -hashfile Dugle-150.0.7871.181-windows-x64.zip SHA256   # Windows

macOS: clear the quarantine flag first

Dugle.app is ad-hoc signed (info.duckmartians.dugle, no Apple Developer ID). Anything downloaded through a browser also carries com.apple.quarantine, and Gatekeeper refuses that combination — often with a misleading "Dugle is damaged and can't be opened". Strip the attribute once, after unpacking:

xattr -dr com.apple.quarantine Dugle.app
./Dugle.app/Contents/MacOS/Dugle --version      # → Dugle 150.0.7871.181

Launch the executable inside the bundle (Dugle.app/Contents/MacOS/Dugle) when you pass flags; open -a Dugle --args … works too, but the direct path is what automation should use. The bare binary cannot run outside the bundle — it needs Contents/Frameworks.

Windows

Unblock the zip before extracting (Properties → Unblock), or:

Unblock-File .\Dugle-150.0.7871.181-windows-x64.zip
Expand-Archive .\Dugle-150.0.7871.181-windows-x64.zip -DestinationPath .
.\Dugle\chrome.exe --version

The executable is still named chrome.exe (a Chromium build convention); the product identity — window title, chrome://version, install paths — reads Dugle.


Quick start

Two profiles, two devices, side by side:

# macOS
./Dugle.app/Contents/MacOS/Dugle --fingerprint=1111 --fingerprint-os=macos \
    --profile-label=alice --user-data-dir=./profiles/alice &
./Dugle.app/Contents/MacOS/Dugle --fingerprint=2222 --fingerprint-os=macos \
    --profile-label=bob   --user-data-dir=./profiles/bob &
# Windows
.\Dugle\chrome.exe --fingerprint=1111 --fingerprint-os=windows `
    --profile-label=alice --user-data-dir=.\profiles\alice

Open examples/fingerprint-report.html in each window. The two must disagree on every hashed row, and each must be identical to itself on the next launch:

--fingerprint=111111 --fingerprint=222222
Apple M2 Pro · 12 cores · 1512×982 Apple M3 Max · 16 cores · 1728×1117
device id b49d03cf device id c54c0735

Both screenshots are from the same machine, the same binary, in the same minute — only the seed differs.


Command-line flags

These three are what Dugle adds. Everything else Chromium accepts still works.

Flag Values Default if omitted
--fingerprint=<seed> any string — a number, a username, a UUID; it is hashed either way absent = spoofing off. The browser reports the real device
--fingerprint-os=<os> macos (or mac), linux, windows windows — and anything unrecognised also means windows
--profile-label=<name> free text no label

Notes that matter in practice:

  • No seed means no spoofing. There is no "default fake device". If you forget --fingerprint, the page sees your real GPU, your real core count, your real screen. This is deliberate — it makes an unseeded launch obviously wrong instead of quietly generic.
  • --fingerprint-os never fails loudly. A typo (--fingerprint-os=mcos) silently means Windows. Check the WebGL renderer string after changing it.
  • --profile-label is not visible to web pages. It is read only by the browser process and is deliberately not forwarded to renderers, so no site can read which profile it is talking to. It shows up in two places for the human: the window title becomes [alice] Example Domain, and on https:// pages the omnibox security chip is filled with the label. On pages where the chip already says something (a certificate warning, http://), the warning wins — a label never hides a security state.

Flags that are Chromium's but that you will almost always want alongside them:

--user-data-dir=<path>     one directory per profile; never share one between two live browsers
--window-size=1280,800     vary it per profile — an identical window size across 50 profiles is a pattern
--no-first-run --no-default-browser-check
--disable-blink-features=AutomationControlled     keeps navigator.webdriver false under automation

What is spoofed, and how

Everything in this table is native C++ inside the binary, driven by the seed. "Deterministic" means the same seed produces the same value on every launch, on every machine.

Surface Mechanism
UNMASKED_RENDERER_WEBGL, UNMASKED_VENDOR_WEBGL Substitution. The seed picks one row out of a per-OS device table; the vendor string comes from the same row, so "NVIDIA" never pairs with an AMD renderer.
Canvas 2D — getImageData, toDataURL, toBlob Deterministic noise: the low bit of one colour channel is flipped on 32 seed-chosen pixels. Invisible to the eye, decisive for a hash.
WebGL readPixels The same pixel perturbation, applied to RGBA/UNSIGNED_BYTE reads — the packing fingerprinters actually use.
AudioContext / OfflineAudioContext The low 2 mantissa bits of each float sample are set (not added) to a seed-derived value, so reading the same buffer twice gives the same answer instead of drifting.
getBoundingClientRect, getClientRects A per-seed multiplicative factor of about ±0.0003 %, fixed for the lifetime of the document.
navigator.hardwareConcurrency The device row's core count (8, 10, 12, 16, 20 …).
navigator.deviceMemory The device row's memory. Always 8 — the specification caps the reported value at 8 GB, so every plausible row says 8.
screen.width / .height / .availWidth / .availHeight The device row's resolution; avail* subtracts 40 px for a menu bar or taskbar.
Installed fonts Allow-list masking. A font family outside the per-OS list resolves as "not installed" and falls back naturally.

Two properties of this design are worth stating outright because they are the ones detectors look for:

  • Nothing is random per launch. Ungoogled-chromium's own anti-fingerprinting re-randomises canvas and client-rect noise on every page load, which is itself a signal: a real device does not change its canvas hash between reloads. Dugle derives everything from the seed, so a profile is stable, which is what a real machine is.
  • Fonts can only be hidden, never invented. The masking list is an intersection with what the host actually has. A macOS profile on a Windows host will not gain Helvetica Neue. This is one of the reasons both builds exist.

The device pools

The seed indexes into a table chosen by --fingerprint-os:

--fingerprint-os rows example renderer
windows 23 ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0, D3D11)
macos 10 ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Max, Unspecified Version)
linux 7 ANGLE (Intel, Mesa Intel(R) Iris(R) Xe Graphics (TGL GT2), OpenGL 4.6)

Screen sizes follow the platform: Windows rows use 1920×1080 / 2560×1440 / 3840×2160 / 1536×864; the macOS rows in this build use Retina logical sizes (1512×982, 1728×1117, 1470×956), which is what a Mac reports at DPR 2.


What is not spoofed

This is the part most tools leave out. Dugle changes the device; it does not change where that device appears to be, or who it says it is. If you launch it bare, the following come from the real host:

Surface Where it comes from Who has to fix it
navigator.platform, navigator.userAgent, Sec-CH-UA*, navigator.userAgentData The real host OS. --fingerprint-os=windows on a Mac still reports MacIntel. The launcher, over CDP — see Driving Dugle from code
Time zone, navigator.language, geolocation The host's settings The launcher, derived from the proxy's exit IP
Public IP, WebRTC candidates The real connection A proxy, plus --force-webrtc-ip-handling-policy=disable_non_proxied_udp
navigator.webdriver true under plain automation --disable-blink-features=AutomationControlled, and dropping Playwright's --enable-automation
screen.colorDepth, devicePixelRatio The real display. Measured on one Retina Mac: depth 30, DPR 2 on the internal panel, depth 24, DPR 1 elsewhere — with the identical seed. Nothing yet. See below.

The last row is a genuine limitation, so be concrete about what it costs: a Windows profile run on a Mac build reports a Windows GPU and a 1920×1080 screen, but colorDepth 30 — which Windows does not report. That combination is inconsistent, and inconsistency is exactly what a fingerprinter grades on.

So: run Windows profiles on the Windows build and macOS profiles on the macOS build. Cross-OS spoofing works for a quick look, not for a profile you intend to keep.


Choosing a device: seeds and --fingerprint-os

A seed is any string. What matters is that you keep it with the profile, forever:

profile "alice"  →  --fingerprint=1487643476  --fingerprint-os=windows  --user-data-dir=…/alice

Store the three together. A profile whose GPU changes between sessions is more suspicious than one with a boring GPU.

Practical guidance:

  • Pick seeds randomly, not 1, 2, 3. Sequential seeds don't correlate the devices (the hash destroys the ordering), but they do tell anyone who sees your config how you generate them.
  • A seed is not a secret, and it is not a lottery ticket either: the pool has 23 Windows devices, so two profiles out of dozens will eventually share a GPU row. They still differ on canvas, audio, and client rects, which is where the entropy is.
  • Changing the seed changes the machine. Do it to escape a burnt fingerprint, understanding that from the site's point of view the account just moved to a new computer — while keeping the same cookies, which is its own kind of odd.
  • --fingerprint-os should match the build you are running on for anything long-lived, for the reasons in the previous section.

Profiles and data directories

One --user-data-dir per profile, and never two live browsers on the same one — Chromium's singleton lock will kill one of them, and a half-written profile is worse than a lost session.

profiles/
  alice/        ← cookies, local storage, extensions, history
  bob/

Without --user-data-dir, Dugle uses its own default (%LOCALAPPDATA%\Dugle\User Data on Windows, ~/Library/Application Support/Dugle on macOS), which is fine for one manual profile and wrong for everything else.

The directory is an ordinary Chromium profile: back it up by copying it, move it between machines of the same OS, delete it to reset the identity. The seed is not stored inside it — pass the same --fingerprint next time, or the profile keeps its cookies while changing its hardware.


Driving Dugle from code

Dugle is Chromium, so every Chromium automation stack works: Playwright, Puppeteer, Selenium, raw CDP. Three ready-to-run examples ship in examples/:

file what it shows
launch_basic.py the minimum: point Playwright at the binary, pass the flags, keep a persistent profile
launch_with_proxy.py the full coherent launch — proxy, time zone and locale from the exit IP, Client Hints, WebRTC
drive_over_cdp.py keep a window open and drive it from separate processes, so a person can watch and take over

The minimum

ctx = pw.chromium.launch_persistent_context(
    user_data_dir="profiles/alice",
    executable_path="./Dugle.app/Contents/MacOS/Dugle",
    headless=False,
    no_viewport=True,                                   # let the real window size drive innerWidth
    ignore_default_args=["--enable-automation"],        # drop Playwright's automation flag
    args=[
        "--fingerprint=1487643476",
        "--fingerprint-os=macos",
        "--profile-label=alice",
        "--disable-blink-features=AutomationControlled",
        "--window-size=1280,800",
    ],
)

launch_persistent_context, not launch: the persistent user-data-dir is the profile. no_viewport=True matters more than it looks — without it Playwright pins an identical 1280×720 viewport on every profile, so fifty "different" machines all report the same innerWidth.

The full launch — what Duck Login (Outis) does per profile

Outis / Duck Login is the profile manager this browser was built for. Its engine is ~80 lines, and every line of it exists to close one of the gaps in What is not spoofed. In order:

  1. Resolve the proxy's exit — one request through the proxy to an IP-geolocation service returns the exit IP, its time zone, country, and coordinates.
  2. Hand those to the contexttimezone_id, a locale derived from the country, and geolocation coordinates. Geolocation permission stays at "prompt": a fresh profile that has already granted location is itself unusual.
  3. Bind the proxy with credentials split out of the URL, the way Playwright wants them.
  4. Block the WebRTC leak — an HTTP proxy carries no UDP, so ICE would go around it and publish the real IP. --force-webrtc-ip-handling-policy=disable_non_proxied_udp.
  5. Override UA, navigator.platform and Client Hints over CDPNetwork.setUserAgentOverride on every page, including pages that open later, with a brand list that re-adds "Google Chrome" (plain Chromium omits it, and "UA says Chrome, hints say only Chromium" is its own mismatch).
  6. Vary the window size per profile from a small pool, all of them small enough to fit inside the smallest screen in the device table — a 1366×768 device reporting a 1920-pixel window is a contradiction.
  7. Fail open, never closed. If the geo lookup is down, the browser still launches and keeps the host time zone. Losing a time zone is a small incoherence; losing the session is a big one.

If the lookup fails and the profile matters, skip the launch rather than run it incoherently — that decision belongs to your manager, not to the browser.

Over CDP, for a window a human shares

launch_persistent_context ties the browser's life to one process. When you want a window that stays open — for supervised work, a login someone has to finish by hand, a session you watch — start it detached with a debugging port and attach on demand:

Dugle --fingerprint=42 --user-data-dir=./profiles/alice --remote-debugging-port=9333
browser = pw.chromium.connect_over_cdp("http://127.0.0.1:9333")
page = browser.contexts[0].pages[0]
page.goto("https://example.com")
browser.close()          # detaches; the window stays open

A page can look for signs of an open debugging port. Use this for work you are supervising, not for hostile environments.


Verifying a build

Open examples/fingerprint-report.html — no network, no third-party service. Then check three things:

  1. Stability — launch the same seed twice. Every hashed row and the combined device id must be identical. If they drift, the seed is not reaching the renderer.
  2. Distinctness — launch two different seeds. canvas 2D, WebGL readPixels, AudioContext, client rects and the device id must all differ.
  3. The negative control — launch with no --fingerprint at all. You should see your real GPU and core count. If a bare launch looks spoofed, something else on the machine is lying to you, and you cannot trust the other two results.

Then confirm against a third-party checker such as browserleaks.com/webgl, browserleaks.com/canvas, or creepjs, which is where the interesting disagreements show up.

A quick automated version of (1) and (2):

python examples/launch_basic.py 1111    # note the device id
python examples/launch_basic.py 1111    # must match
python examples/launch_basic.py 2222    # must differ

Building from source

The published binaries are built from ungoogled-chromium 150.0.7871.181 plus a patch set that adds the flags and the spoofing described above:

  • clone ungoogled-chromium-macos / ungoogled-chromium-windows at that tag, run their retrieve / prune / patch / domain-substitution steps
  • add the fingerprint sources under third_party/blink/renderer/platform/, and apply the consumer patches to the WebGL, canvas, audio, screen, font-cache and document call sites
  • forward fingerprint and fingerprint-os to the renderer in render_process_host_impl.cc — without this the renderer never sees the seed and every patch is a no-op
  • build with is_official_build=true; for iteration, use_thin_lto=false is_cfi=false chrome_pgo_phase=0 turns a link from minutes into seconds

A full build is roughly a day per platform, most of it unattended compiling.

The patch set is not part of this repository. Open an issue if you need it.


Licensing and attribution

Dugle is a build of Chromium (BSD-3-Clause) with the ungoogled-chromium patch set applied. Neither project is affiliated with this build, and the name and icon are not theirs.

The complete third-party attribution ships inside the browser — open chrome://credits, which renders the full ~19 MB license list for every bundled component, and states that ungoogled-chromium's source is available in its repository. Chromium's own attribution string is intact: "Dugle is made possible by the Chromium open source project and other open source software."

See THIRD-PARTY.md for exactly what is in the binary and where each upstream license lives.


What Dugle is not

  • Not an anonymity tool. It makes a browser look like a consistent, ordinary machine. It does not hide who you are from anyone who can see your traffic, and a stable fingerprint is by design trackable — that is the point.
  • Not a proxy. No IP is changed until you give it one.
  • Not a guarantee. Fingerprinting is an arms race against code that is updated more often than this browser is. Verify against real checkers, on the platform you actually ship, before you rely on it.