Cases / #4 · 2026-09-30 · Hard
A German Windows persona with Camoufox — and the one setting that broke it
OS, locale, screen, window, WebRTC and geo-IP in one consistent fingerprint. The proxy decides whether the story holds.
Run on production on 2026-09-30: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Camoufox (a hardened Firefox) generates a whole device fingerprint from a few options. The goal: a German user on a Windows desktop — German language headers, a Windows user agent and platform, a common screen size, no WebRTC leak, and a timezone that matches where the IP is. We run the same persona twice: through a random residential exit, and through one in Germany.
What we used, and why
| What | Why |
|---|---|
os: "windows" | Windows user agent, navigator.platform: Win32, Windows-typical fonts and GPU strings. |
locale: "de-DE" | navigator.languages and the Accept-Language header in German. |
screen, window | Pin a common 1920×1080 screen and a 1600×900 window instead of random sizes. |
humanize: true | Human-like cursor movement for anything you click. |
block_webrtc: true | No RTCPeerConnection, so WebRTC can't leak the real network path. |
geoip: true | Timezone and geolocation follow the proxy's exit IP. This is the option that makes or breaks the persona. |
PROXY_URL_DE | The same residential provider with a German exit (most providers take a country in the username, e.g. -country-de). |
How it works
- Launch Camoufox with the persona through a random exit, read what the page and the network see, then look up the exit IP's country.
- Do the same through a German exit.
- Compare: do language, timezone and exit country tell the same story?
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000PROXY_URL_DE— a proxy exit in Germany
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL (any exit), PROXY_URL_DE (an exit in Germany)
import { firefox } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
// A German Windows desktop: every value below is part of one consistent story.
const persona = (proxy) => ({
proxy,
headless: true,
os: 'windows',
locale: 'de-DE',
screen: { minWidth: 1920, maxWidth: 1920, minHeight: 1080, maxHeight: 1080 },
window: [1600, 900],
humanize: true,
block_webrtc: true,
geoip: true, // timezone and geolocation follow the proxy's exit IP
});
async function run(proxy) {
const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify(persona(proxy)),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await firefox.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
const page = await browser.newPage();
const fp = await (await page.goto('https://tls.peet.ws/api/all', { timeout: 60000 })).json();
const seen = await page.evaluate(() => {
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl?.getExtension('WEBGL_debug_renderer_info');
return {
platform: navigator.platform,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${screen.width}x${screen.height}`,
window: `${outerWidth}x${outerHeight}`,
hardware_concurrency: navigator.hardwareConcurrency,
webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
webrtc: typeof RTCPeerConnection !== 'undefined',
};
});
const headers = fp.http2.sent_frames.find((f) => f.frame_type === 'HEADERS').headers;
// Where the proxy exits, as a website would look it up.
const geo = await (await page.goto('http://ip-api.com/json/?fields=country,timezone', { timeout: 60000 })).json();
return {
exit_country: geo.country,
exit_timezone: geo.timezone,
user_agent: fp.user_agent,
accept_language: headers.find((h) => h.startsWith('accept-language: '))?.slice(17) ?? null,
...seen,
ja4: fp.tls.ja4,
};
} finally {
await browser.close();
}
}
console.log(JSON.stringify({
'random exit': await run(process.env.PROXY_URL),
'German exit': await run(process.env.PROXY_URL_DE),
}, null, 2));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL (any exit), PROXY_URL_DE (an exit in Germany)
import json
import os
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
def persona(proxy):
# A German Windows desktop: every value below is part of one consistent story.
return {
"proxy": proxy,
"headless": True,
"os": "windows",
"locale": "de-DE",
"screen": {"minWidth": 1920, "maxWidth": 1920, "minHeight": 1080, "maxHeight": 1080},
"window": [1600, 900],
"humanize": True,
"block_webrtc": True,
"geoip": True, # timezone and geolocation follow the proxy's exit IP
}
PAGE_SIGNALS = """() => {
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
platform: navigator.platform,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${screen.width}x${screen.height}`,
window: `${outerWidth}x${outerHeight}`,
hardware_concurrency: navigator.hardwareConcurrency,
webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
webrtc: typeof RTCPeerConnection !== 'undefined',
};
}"""
def run(p, proxy):
res = requests.post("https://starter.cdpfleet.com/camoufox/session", headers={"x-api-key": KEY},
json=persona(proxy), timeout=60)
res.raise_for_status()
browser = p.firefox.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
page = browser.new_page()
fp = page.goto("https://tls.peet.ws/api/all", timeout=60000).json()
seen = page.evaluate(PAGE_SIGNALS)
headers = next(f for f in fp["http2"]["sent_frames"] if f["frame_type"] == "HEADERS")["headers"]
# Where the proxy exits, as a website would look it up.
geo = page.goto("http://ip-api.com/json/?fields=country,timezone", timeout=60000).json()
return {
"exit_country": geo["country"],
"exit_timezone": geo["timezone"],
"user_agent": fp["user_agent"],
"accept_language": next((h[17:] for h in headers if h.startswith("accept-language: ")), None),
**seen,
"ja4": fp["tls"]["ja4"],
}
finally:
browser.close()
with sync_playwright() as p:
print(json.dumps({
"random exit": run(p, os.environ["PROXY_URL"]),
"German exit": run(p, os.environ["PROXY_URL_DE"]),
}, indent=2))
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY, PROXY_URL (any exit), PROXY_URL_DE (an exit in Germany)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
// A German Windows desktop: every value below is part of one consistent story.
static String persona(String proxy) {
return """
{
"proxy": %s,
"headless": true,
"os": "windows",
"locale": "de-DE",
"screen": {"minWidth": 1920, "maxWidth": 1920, "minHeight": 1080, "maxHeight": 1080},
"window": [1600, 900],
"humanize": true,
"block_webrtc": true,
"geoip": true
}""".formatted(new Gson().toJson(proxy)); // geoip: timezone and location follow the exit IP
}
static final String PAGE_SIGNALS = """
() => {
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
platform: navigator.platform,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${screen.width}x${screen.height}`,
window: `${outerWidth}x${outerHeight}`,
hardware_concurrency: navigator.hardwareConcurrency,
webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
webrtc: typeof RTCPeerConnection !== 'undefined',
};
}""";
static JsonObject run(Playwright playwright, String proxy) throws Exception {
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/camoufox/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(persona(proxy))).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
Browser browser = playwright.firefox().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Page page = browser.newPage();
JsonObject fp = JsonParser.parseString(page.navigate("https://tls.peet.ws/api/all",
new Page.NavigateOptions().setTimeout(60000)).text()).getAsJsonObject();
JsonObject seen = new Gson().toJsonTree(page.evaluate(PAGE_SIGNALS)).getAsJsonObject();
String acceptLanguage = null;
for (JsonElement f : fp.getAsJsonObject("http2").getAsJsonArray("sent_frames")) {
if (!f.getAsJsonObject().get("frame_type").getAsString().equals("HEADERS")) continue;
for (JsonElement h : f.getAsJsonObject().getAsJsonArray("headers")) {
if (h.getAsString().startsWith("accept-language: ")) acceptLanguage = h.getAsString().substring(17);
}
}
// Where the proxy exits, as a website would look it up.
JsonObject geo = JsonParser.parseString(page.navigate("http://ip-api.com/json/?fields=country,timezone",
new Page.NavigateOptions().setTimeout(60000)).text()).getAsJsonObject();
JsonObject out = new JsonObject();
out.add("exit_country", geo.get("country"));
out.add("exit_timezone", geo.get("timezone"));
out.add("user_agent", fp.get("user_agent"));
out.addProperty("accept_language", acceptLanguage);
for (String k : seen.keySet()) out.add(k, seen.get(k));
out.add("ja4", fp.getAsJsonObject("tls").get("ja4"));
return out;
} finally {
browser.close();
}
}
public static void main(String[] args) throws Exception {
try (Playwright playwright = Playwright.create()) {
JsonObject out = new JsonObject();
out.add("random exit", run(playwright, System.getenv("PROXY_URL")));
out.add("German exit", run(playwright, System.getenv("PROXY_URL_DE")));
System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL (any exit), PROXY_URL_DE (an exit in Germany)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
// A German Windows desktop: every value below is part of one consistent story.
object Persona(string proxy) => new
{
proxy,
headless = true,
os = "windows",
locale = "de-DE",
screen = new { minWidth = 1920, maxWidth = 1920, minHeight = 1080, maxHeight = 1080 },
window = new[] { 1600, 900 },
humanize = true,
block_webrtc = true,
geoip = true, // timezone and geolocation follow the proxy's exit IP
};
const string PageSignals = """
() => {
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
platform: navigator.platform,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: `${screen.width}x${screen.height}`,
window: `${outerWidth}x${outerHeight}`,
hardware_concurrency: navigator.hardwareConcurrency,
webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
webrtc: typeof RTCPeerConnection !== 'undefined',
};
}
""";
async Task<JsonObject> Run(string proxy)
{
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/camoufox/session", Persona(proxy));
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
var browser = await playwright.Firefox.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var page = await browser.NewPageAsync();
var fp = JsonNode.Parse(await (await page.GotoAsync("https://tls.peet.ws/api/all", new() { Timeout = 60000 }))!.TextAsync())!;
var seen = JsonNode.Parse((await page.EvaluateAsync<JsonElement>(PageSignals)).GetRawText())!.AsObject();
var headers = fp["http2"]!["sent_frames"]!.AsArray().First(f => (string?)f!["frame_type"] == "HEADERS")!["headers"]!.AsArray();
// Where the proxy exits, as a website would look it up.
var geo = JsonNode.Parse(await (await page.GotoAsync("http://ip-api.com/json/?fields=country,timezone", new() { Timeout = 60000 }))!.TextAsync())!;
var outp = new JsonObject
{
["exit_country"] = geo["country"]!.DeepClone(),
["exit_timezone"] = geo["timezone"]!.DeepClone(),
["user_agent"] = fp["user_agent"]!.DeepClone(),
["accept_language"] = headers.Select(h => (string)h!).FirstOrDefault(h => h.StartsWith("accept-language: "))?[17..],
};
foreach (var (k, v) in seen) outp[k] = v?.DeepClone();
outp["ja4"] = fp["tls"]!["ja4"]!.DeepClone();
return outp;
}
finally
{
await browser.CloseAsync();
}
}
var result = new JsonObject
{
["random exit"] = await Run(Environment.GetEnvironmentVariable("PROXY_URL")!),
["German exit"] = await Run(Environment.GetEnvironmentVariable("PROXY_URL_DE")!),
};
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL (any exit), PROXY_URL_DE (an exit in Germany)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
// A German Windows desktop: every value below is part of one consistent story.
func persona(proxy string) map[string]any {
return map[string]any{
"proxy": proxy,
"headless": true,
"os": "windows",
"locale": "de-DE",
"screen": map[string]int{"minWidth": 1920, "maxWidth": 1920, "minHeight": 1080, "maxHeight": 1080},
"window": []int{1600, 900},
"humanize": true,
"block_webrtc": true,
"geoip": true, // timezone and geolocation follow the proxy's exit IP
}
}
const pageSignals = `() => {
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
platform: navigator.platform,
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: screen.width + 'x' + screen.height,
window: outerWidth + 'x' + outerHeight,
hardware_concurrency: navigator.hardwareConcurrency,
webgl_renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
webrtc: typeof RTCPeerConnection !== 'undefined',
};
}`
func run(pw *playwright.Playwright, proxy string) map[string]any {
session, err := launch("camoufox", persona(proxy))
if err != nil {
log.Fatal(err)
}
browser, err := pw.Firefox.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
defer browser.Close()
page, _ := browser.NewPage()
res, err := page.Goto("https://tls.peet.ws/api/all", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
if err != nil {
log.Fatal(err)
}
var fp struct {
UserAgent string `json:"user_agent"`
TLS struct {
JA4 string `json:"ja4"`
} `json:"tls"`
HTTP2 struct {
SentFrames []struct {
FrameType string `json:"frame_type"`
Headers []string `json:"headers"`
} `json:"sent_frames"`
} `json:"http2"`
}
res.JSON(&fp)
seen, err := page.Evaluate(pageSignals)
if err != nil {
log.Fatal(err)
}
// Where the proxy exits, as a website would look it up.
geoRes, err := page.Goto("http://ip-api.com/json/?fields=country,timezone", playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
if err != nil {
log.Fatal(err)
}
var geo struct{ Country, Timezone string }
geoRes.JSON(&geo)
out := seen.(map[string]any)
out["exit_country"], out["exit_timezone"], out["user_agent"], out["ja4"] = geo.Country, geo.Timezone, fp.UserAgent, fp.TLS.JA4
out["accept_language"] = nil
for _, f := range fp.HTTP2.SentFrames {
for _, h := range f.Headers {
if f.FrameType == "HEADERS" && strings.HasPrefix(h, "accept-language: ") {
out["accept_language"] = h[17:]
}
}
}
return out
}
func main() {
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
out, _ := json.MarshalIndent(map[string]any{
"random exit": run(pw, os.Getenv("PROXY_URL")),
"German exit": run(pw, os.Getenv("PROXY_URL_DE")),
}, "", " ")
fmt.Println(string(out))
}
What we got
| Exit | IP country | Browser timezone | Accept-Language | Platform | Screen | Window | WebRTC |
|---|---|---|---|---|---|---|---|
| random exit | United States | Asia/Kuala_Lumpur | de-DE,de;q=0.9 | Win32 | 1920x1080 | 1600x900 | no |
| German exit | Germany | Europe/Berlin | de-DE,de;q=0.9 | Win32 | 1920x1080 | 1600x900 | no |
From the Node.js run on 2026-09-30. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
{
"random exit": {
"exit_country": "United States",
"exit_timezone": "America/New_York",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
"accept_language": "de-DE,de;q=0.9",
"platform": "Win32",
"languages": [
"de-DE",
"de"
],
"timezone": "Asia/Kuala_Lumpur",
"screen": "1920x1080",
"window": "1600x900",
"hardware_concurrency": 24,
"webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar",
"webrtc": false,
"ja4": "t13d1617h2_86a278354501_3cbfd9057e0d"
},
"German exit": {
"exit_country": "Germany",
"exit_timezone": "Europe/Berlin",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
"accept_language": "de-DE,de;q=0.9",
"platform": "Win32",
"languages": [
"de-DE",
"de"
],
"timezone": "Europe/Berlin",
"screen": "1920x1080",
"window": "1600x900",
"hardware_concurrency": 32,
"webgl_renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
"webrtc": false,
"ja4": "t13d1617h2_86a278354501_3cbfd9057e0d"
}
}Takeaways
- Everything Camoufox controls came out as asked in both runs: Windows user agent and platform,
de-DElanguages and header, 1920×1080 screen, 1600×900 window, no WebRTC, a plausible Windows GPU. - The random exit broke the story: a German-speaking Windows user in whatever timezone the random exit happened to be in (see the table).
geoipdid its job — it matched the IP — but the IP didn't match the persona. Choose the proxy country to match the locale. - Rotating residential proxies can split the story further: the timezone is set from the exit IP at launch, and a rotating proxy may exit somewhere else a moment later — in some of our runs the browser timezone and the IP a website saw were in different countries. Use sticky sessions (same IP for the whole session) with
geoip. - The German exit is consistent end to end:
de-DE,Europe/Berlin, German IP. - Fingerprint values that Camoufox randomizes (GPU, core count) differ between sessions — that's intended: each session is a different "device".