The Akamai client
clients/akamai warms an Akamai Bot Manager session without a browser and then carries it: it fetches the page, finds the sensor script the page names, runs that script unmodified inside a wre-live realm with the wre-sandbox browser surface, posts what the script builds, and hands the session’s cookies to whatever request you want to make next.
Nothing about the payload is reimplemented. The vendor’s own script computes it, which is why a rebuild of the sensor does not break this client.
wre client describe akamaiThe flow
Section titled “The flow”solve does the whole warm-up:
GETthe page with navigation headers, keeping cookies in a per-session jar.- Read the surface out of the HTML: the sensor script, the pixel client, the
bazadebezolkohpepadrseed and the config bits the script’s own URL carries. GETthe sensor script with script headers and the page as referer.- Mount the page in the sandbox: the profile’s device values, the page’s own HTML with its forms and inputs,
document.cookiewired to the jar, requests wired to the host. - Run the script, fire
readystatechange,DOMContentLoadedandload, play a pointer, click and key stream, then let the clock run. - Read
bmak.get_telemetry(), post the payload it carries to the collection endpoint, and repeat for as many rounds as asked.
After that, request sends anything you like with the session’s cookies, and --telemetry attaches a fresh akamai-bm-telemetry header built from the same realm.
{ "op": "solve", "params": { "url": "https://login.example.com/" } }{ "op": "request", "params": { "url": "https://api.example.com/orders", "telemetry": true } }The other ops: discover reports the surface without running anything, page hands back the page HTML and its form fields, payload builds a fresh payload from the open session, post posts one, cookies reads the jar, pow answers a proof-of-work challenge, pixel runs the pixel client, reset drops the sandbox.
What the config controls
Section titled “What the config controls”| field | default | what it does |
|---|---|---|
page_url |
none | page the session warms against when an op does not name one |
profile |
first in the library | sandbox profile id from wre sandbox list |
random_profile |
false |
pick a captured profile at random instead |
random_machine |
true |
put every session on a different Mac: chip, cores and memory, picked together |
fingerprint |
from the user agent | transport fingerprint as profile[:platform] |
user_agent |
the profile’s | overrides what the sandbox and the transport both claim |
behaviour |
true |
play a pointer, click and key stream into the page |
typed |
a word off the seed | what the visitor types into the page’s first text field |
pixel |
true |
run the pixel client when the page serves one |
live_xhr |
true |
let the sensor’s own requests leave the sandbox |
wire_pace |
false |
hold each request until the wall clock reaches the sandbox’s |
warp |
16 |
how much faster the page’s clock runs than the wall while a wait is running |
load_posts_ms |
4000 |
ceiling on waiting for the script’s own load posts to be answered |
wait_ms |
6000 |
ceiling on the wait after the input stream, which ends when the edge validates the jar |
rounds |
2 |
payloads the host posts per solve, when live_xhr is off |
init_cost_ms |
25 |
clock charge applied when the script writes bmak.startTs |
The two waits are ceilings on a condition, not durations: the first ends when the script’s own load posts have been answered, the second when the edge has validated the jar. A session the edge believes early pays neither, which is most of why a solve takes about a second and a half.
live_xhr is the one worth understanding. With it on, which is the default, every request the script makes goes out for real, the way it does in a browser. With it off the sandbox answers the script’s own XMLHttpRequest with 201 {"success":true} and the host posts the payload the script built instead, once per round. The edge reads those two differently: measured against Xero’s login endpoint, and again against the Lee County search, the same payload from the same profile over the same transport is served one way and refused the other.
The device profile
Section titled “The device profile”The sandbox profile is where the payload’s device fields come from. The built-in one is a whole captured Chrome on macOS, so a fresh install is served, but it carries no page layout and a capture of your own is still better. wre sandbox capture serves a page you open in the browser you want to replay, --open drives a real Chrome instead; the client picks the first profile in profiles/ unless told otherwise, and falls back to the built-in when the library is empty.
What a machine renders is left exactly as captured, because two Macs running the same macOS with the same fonts draw the same canvas: text rasterises on the CPU. A canvas hash that moves per session belongs to no machine at all, and the edge notices. What does differ between Macs is the chip, its core count and its memory, so random_machine puts each session on one of sixteen real combinations from M1 to M4 Pro and rewrites the renderer string, hardwareConcurrency and deviceMemory together. run.machine reports what a session picked.
Keep the transport and the profile in step. The client sends the profile’s user agent and lets wre-net pick the closest emulation profile for it. If the captured Chrome is newer than any profile wreq carries, the handshake lands on the newest one it has while the header says what the device said.
What was measured
Section titled “What was measured”Against https://login.xero.com/identity/user/login, which refuses a request whose session never posted a believable payload and answers a credential error when it accepts one:
| arm | login answer |
|---|---|
| no sensor run at all | 403, Akamai access denied |
the word garbage posted as the payload |
403 |
| what this client builds | 200, the credential error page |
clients/akamai/tests/gate.rs is those three arms; it reaches the live endpoint and is #[ignore]d by default.
cargo test -p wre-client-akamai --test gate -- --ignored --test-threads=1_abck stays at field 2 -1 through all of it, including the arm that passes, so whatever that endpoint reads it is not the cookie’s validation state. Every payload post answers 201 regardless of what is in it, so a 201 there means nothing on its own.
The offline suite in clients/akamai/tests/session.rs runs the same client against a local edge that serves its own sensor script, and covers discovery, the sandbox run, the posts, the pixel client, the cookie jar and a protected endpoint that refuses a session it did not warm.
Proof of work
Section titled “Proof of work”The challenge arrives in _abck field 4 as <id>-<salt>-<difficulty>-<delay>-<slice>[-<version>], several separated by ||. The pow op parses them, takes the first (preferring version 2), and searches ten rounds: round n uses difficulty + n as the modulus and looks for a nonce where sha256(token + startTs + salt + modulus + nonce), read as a big-endian integer, is zero modulo that modulus. The answer is formatted as the four ;-separated lists the script publishes.
{ "op": "pow", "params": { "abck": "<cookie>", "start_ts": 1760000000000 } }No host we have looked at asks for one: field 4 is -1 everywhere, and the config bit that enables the client-side solver is off. The search and the answer format are checked against the format the script parses and against themselves, never against a live challenge.
The pixel challenge
Section titled “The pixel challenge”When the page carries a bazadebezolkohpepadr seed and an /akam/<gen>/<hash> script alongside an obfuscated sensor, that script is the pixel client. The client seeds the realm, runs it in the same sandbox, and replays the form-encoded POST it produces to pixel_<hash>, where the hash is (77 ^ seed).toString(16). As with the sensor, nothing about that payload is rebuilt here.
What is not handled
Section titled “What is not handled”sec_cpt is parsed, not solved. The cookie, the challenge state in _abck and a challenge interstitial in the page are all reported in solve’s challenge field, and that is where it stops: no host in the research set ever served one, so there is nothing to write an answer against that would not be a guess.
The built-in profile has no speech synthesis voices and no audio render hash, so a run on it records those two misses. A captured profile fills them.