Commit Graph

2 Commits

Author SHA1 Message Date
sulthan 1552dd15da fix(docker): correct the timezone claim — UTC is the tell, not a country mismatch
The previous commit documented the clock-zone requirement as "the zone
must match the egress IP's country". Re-measuring against the deployment
case shows that is wrong.

The original inference came from reading the host's /etc/timezone
(Asia/Bangkok) and assuming the egress IP was Thai. It is not: this host
egresses from an Indonesian IP. Asia/Bangkok cleared the challenge not
because it matched a country but because it is simply not UTC, and the
two share +07, which hid the distinction.

Measured 2026-08-08, identical container, one Indonesian egress IP:

  TZ=UTC               never cleared (60s, twice)
  TZ=Asia/Jakarta      cleared in 4s
  TZ=America/New_York  cleared in 4s

America/New_York matches neither the country nor the offset nor the
hemisphere, and clears just as fast. So a UTC clock is itself the bot
signal - Cloudflare scores it as the datacenter default - and any real
zone satisfies the check.

This makes the knob considerably less fragile than documented: BROWSER_TZ
needs a plausible zone, not a geolocated one, and a deployment that moves
region does not have to keep it in sync. Comments in chrome/entrypoint.sh
and docker-compose.yml, the hard constraint in AGENTS.md, and the PR
description are corrected accordingly.

BROWSER_TZ is also documented in .env.example for the first time, which
is the file an operator actually copies - the setting decides whether
kagane works at all, and a UTC server (the common case) is exactly the
one that fails with it unset.

Re-verified against the shipped image with TZ=Asia/Jakarta:
TestSmokeKaganeImage PASS (5.00s, 56710 bytes of image/webp),
TestSmokeKaganeGet PASS (1.24s, status 200).
2026-08-08 23:16:39 +07:00
sulthan 5713e3d04a fix(docker): replace headless-shell with real Chrome so kagane's challenge clears
chromedp/headless-shell cannot clear kagane.to's managed challenge. It is
a stripped Chrome build, and the tells are structural rather than a
header: navigator.webdriver is true, the plugin list is empty, and the
client hints are Chromium- rather than Chrome-branded. Overriding
webdriver through CDP was tried on its own and changed nothing.

Everything below was measured on 2026-08-08 from a single IP, against the
same kagane cover, so the comparisons are like for like:

  chromedp/headless-shell:stable   never cleared (90s)
  zenika/alpine-chrome             never cleared - ships Chrome 124, old
                                   enough that Cloudflare refuses it and
                                   old enough to break chromedp's CDP structs
  google-chrome, default UA        never cleared (60s) - --headless=new
                                   advertises "HeadlessChrome"
  google-chrome, stock UA, UTC     never cleared (90s)
  google-chrome, stock UA, TZ set  cleared in ~4s

So both remaining tells are load-bearing, and each was tested in
isolation. chrome/ is a Debian image with google-chrome-stable, a UA
whose version is read back out of the binary at startup (a hardcoded one
would drift out of step with the Sec-CH-UA hints on the next Chrome
update and become a fresh tell), and no --enable-automation.

The timezone matters because Cloudflare scores a browser whose clock zone
disagrees with its egress IP's country as a proxy. Note that the usual
`-v /etc/localtime:/etc/localtime:ro` does not work here: Chrome resolves
the zone through ICU, which takes the name from that path's symlink
target and ignores the file's contents, so glibc reports the host zone
while Chrome still reports UTC. /etc/timezone carries the name and is
mounted instead; BROWSER_TZ overrides it for a host whose clock is UTC in
a country that is not.

Chrome also binds its DevTools port to loopback and silently ignores
--remote-debugging-address, which is why headless-shell fronted it with
socat. This image does the same, so it stays a drop-in: the compose
service keeps the headless-shell name and its pinned address, and
BROWSER_WS_URL is unchanged.

Deploying needs `docker compose build headless-shell`.
2026-08-08 23:03:41 +07:00