#!/bin/sh set -eu # A UTC clock is itself the bot signal — Cloudflare treats it as the datacenter # default — and kagane's challenge then never clears. Measured 2026-08-08 with # an identical container on one Indonesian egress IP: UTC never cleared in 60s # (twice), while Asia/Jakarta and America/New_York both cleared in 4s. Any real # zone will do; the zone does not have to match the IP's country, it just must # not be UTC. It does have to be right the way Chrome reads it. # # TZ must carry the zone *name*. Chrome resolves the zone through ICU, which # takes the name from /etc/localtime's symlink target and ignores the file's # contents; bind-mounting the host's /etc/localtime therefore lands on the # image's own symlink to Etc/UTC and leaves glibc reporting +07 while Chrome # still reports UTC. /etc/timezone, mounted by docker-compose.yml, is the name. [ -n "${TZ:-}" ] || TZ=$(cat /etc/timezone 2>/dev/null || echo UTC) export TZ # Chrome's own UA advertises "HeadlessChrome" under --headless=new, and that one # token is the difference between kagane.to's challenge clearing in ~4s and # never clearing at all (measured 2026-08-08, same host, same Chrome, only the # UA changed). Overriding it does not touch the Sec-CH-UA client hints, which # report the real version, so the version is read back out of the binary rather # than hardcoded: a hardcoded one would drift out of step with the hints on the # next Chrome update and become a fresh tell. major=$(google-chrome-stable --version | sed -E 's/[^0-9]*([0-9]+)\..*/\1/') ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36" # Chrome binds its DevTools port to loopback and silently ignores # --remote-debugging-address (verified 2026-08-08: Chrome 151 with # --remote-debugging-address=0.0.0.0 still listened on 127.0.0.1 only), so the # caller — another container — cannot reach it directly. socat fronting the # loopback port is how chromedp/headless-shell solved the same problem and is # why this image is a drop-in for it. # # Nothing publishes 9222; reachability is the `browser` network in # docker-compose.yml, and an exposed CDP endpoint is remote code execution. socat TCP-LISTEN:9222,fork,reuseaddr TCP:127.0.0.1:9223 & # Chrome stays in the foreground so that its death takes the container down and # compose's restart policy applies; a backgrounded browser behind a live socat # would leave the sidecar looking healthy while answering nothing. # # No --enable-automation: it sets navigator.webdriver, the first thing a bot # check reads. # # --no-sandbox because Chrome's zygote wants user namespaces, which Docker's # default profile does not hand out; the alternative is --cap-add=SYS_ADMIN, # which gives the container strictly more than it takes away. Containment here # is the unprivileged user, the isolated network, and the fact that this # browser only ever navigates to kagane.to and novelfull.com. exec google-chrome-stable \ --headless=new \ --no-sandbox \ --remote-debugging-port=9223 \ --user-agent="$ua" \ --user-data-dir=/home/chrome/profile \ --no-first-run \ --no-default-browser-check \ --disable-gpu \ about:blank