Ekaii-branded shared Anubis anti-scraper gateway
  • Python 77.9%
  • JavaScript 22.1%
Find a file
admin_ekaii a3867245f1
All checks were successful
build-and-deploy / build (push) Successful in 14s
ci-smoke: accept Refresh: header OR <meta> for the no-JS check
metarefresh.go randomly delivers the refresh as a `Refresh:` response header
instead of a body <meta> (showMeta = RandomData[0]%2==0), so the body-only
assertion failed ~half the time. Verified on the 2.4.9 image: PoW page keeps
main.mjs + the ?__nojs=1 noscript bounce; ?__nojs=1 yields a JS-free metarefresh
(header or meta) and the full no-JS round trip passes the clearance cookie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 01:45:24 +02:00
.forgejo/workflows CI: fix smoke policy mount (docker cp instead of host bind via socket) 2026-06-29 00:35:20 +02:00
rebrand nojs: JS clients get PoW, no-JS clients get metarefresh (per-client, no UA sniffing) 2026-06-30 01:37:29 +02:00
test ci-smoke: accept Refresh: header OR <meta> for the no-JS check 2026-06-30 01:45:24 +02:00
.dockerignore Full nopasaran recipe: clean theme + deep JSON schema rename (v2.4.5, live) 2026-06-28 21:34:31 +02:00
.gitignore Full nopasaran recipe: clean theme + deep JSON schema rename (v2.4.5, live) 2026-06-28 21:34:31 +02:00
docker-compose.yml Frosted-glass interstitial (2.4.8): Apple-style translucent card 2026-06-28 23:15:25 +02:00
Dockerfile.ekaii nojs: JS clients get PoW, no-JS clients get metarefresh (per-client, no UA sniffing) 2026-06-30 01:37:29 +02:00
nginx-fanout.conf add live fanout config 2026-06-15 21:38:33 +00:00
policy.yaml nojs: JS clients get PoW, no-JS clients get metarefresh (per-client, no UA sniffing) 2026-06-30 01:37:29 +02:00
README.md CI/CD: Forgejo Actions build->smoke->deploy pipeline 2026-06-29 00:20:13 +02:00
VERSION nojs: JS clients get PoW, no-JS clients get metarefresh (per-client, no UA sniffing) 2026-06-30 01:37:29 +02:00

nopasaran

ekaii's de-fingerprinted fork of Anubis (PoW anti-bot gateway). Same protection as upstream, but rebranded and re-shaped so off-the-shelf Anubis solvers and fingerprinters don't recognise it. Built from source on a v1.25.0 base.

Currently live as the front bot-wall for *.ekaii.fr (NAS Coolify, behind coolify-proxy / Traefik at priority 7000 → nopasaran:8081anubis-fanout → per-service backends).

What the rebrand changes

The build patches the upstream source before npm run assets + go build:

  • definger.py — renames every network-visible Anubis signature: URL paths (/.within.website/.../_ek/...), cookie names, headers, the embedded JSON <script> ids (anubis_challengenop_c, anubis_versionnop_v, ...), and strips the TecharoHQ/Techaro/celphase footer.

  • fix_nop.py — second-pass rename _ek_nop.

  • fix_locales.py — localised strings → neutral branded copy ("Securing your connection"), scrubs the distinctive "Oh noes!" title (a strong Anubis fingerprint) → ekaii.fr, and sets the footer credit keys to non-empty values. (Empty locale values make the i18n layer panic at runtime with message "<key>" not found — see gotchas.)

  • theme.py — replaces the interstitial with a clean, adaptive (light/dark) card + spinner; hides the mascot image.

  • schema_rename.py — the deep one. Renames the challenge JSON schema so a stock Anubis solver (which reads challenge.randomData / rules.difficulty under {challenge, rules}) gets a KeyError, while a nopasaran-aware client reads the renamed fields:

    upstream nopasaran where
    rules (wrapper) q web/index.go
    challenge (wrapper) z web/index.go
    randomData rb lib/challenge/challenge.go
    id k lib/challenge/challenge.go
    issuedAt ts lib/challenge/challenge.go
    method md lib/challenge/challenge.go
    metadata mx lib/challenge/challenge.go
    policyRuleHash ph lib/challenge/challenge.go
    difficulty (Challenge) v2 lib/challenge/challenge.go
    spent sp lib/challenge/challenge.go

    web/js/main.ts is patched in lockstep to read {z: challenge, q: rules}, challenge.rb, challenge.k.

    DO NOT rename (these crash-loop prod or break the PoW contract):

    • config.ChallengeRules tags (algorithm/difficulty/report_as) — that's the q payload the client reads; rules.difficulty drives the PoW.
    • the algorithm registry names (fast/slow/metarefresh).
    • lib/anubis.go's make-challenge struct (rules/challenge/id) — not on the JS path.

Build

The Dockerfile expects the upstream source in ./anubis/:

git clone --branch v1.25.0 --depth 1 https://github.com/TecharoHQ/anubis anubis
docker build -f Dockerfile.ekaii --build-arg VERSION=2.4.5 -t nopasaran:2.4.5 .

The build is self-contained (golang:1.24-bookworm + node 22 + zstd/brotli): npm ci → de-finger scripts → npm run assets (templ generate + esbuild + postcss) → go build.

Deploy (NAS)

Image is built on herbesvertes, loaded onto the NAS over the LAN, then recreated via compose:

# herbesvertes -> NAS (one LAN hop)
ssh <NAS> 'ssh <herbesvertes> "docker save nopasaran:2.4.5 | gzip -1" | docker load'
# bump image + recreate (compose project `anubis-shared`, /root/anubis-shared/)
docker compose -p anubis-shared up -d

ED25519_PRIVATE_KEY_HEX comes from a sibling .env (never committed). The same key must be shared by every nopasaran/anubis instance so the .ekaii.fr cookie (COOKIE_DYNAMIC_DOMAIN) is honoured cluster-wide — one solve = SSO across all subdomains.

Verify

test/nopsolve.py is the verification harness:

  1. parses nop_c, asserts the renamed schema (q/z/rb),
  2. shows a stock solver hitting KeyError: 'challenge' (defeated),
  3. solves the PoW (sha256(rb + nonce), difficulty leading hex zeros), calls pass-challenge, and confirms a real client passes the wall (302 → cookie → 200).

Gotchas (hard-won)

  • Always smoke-test under a throwaway name (status running, restarts=0, no panic) before swapping prod. The schema rename touches the PoW contract — verify a solve round-trips.
  • Empty locale values panic the localizer at request time (message "<key>" not found, 500). Footer/credit keys must be non-empty.
  • X-Real-Ip required: with use-remote-address: false, the policy needs X-Real-Ip / X-Forwarded-For from the proxy; a direct curl without it → 500 [misconfiguration]. Traefik sets it in prod.
  • Testing cookies off-host: COOKIE_DYNAMIC_DOMAIN=true scopes the cookie to .ekaii.fr, so a cookiejar keyed on 127.0.0.1 drops it ("user has cookies disabled"). Replay Set-Cookie manually, or test against the real host.
  • CHALLENGE: 403 is intentional (policy status_codes) — the interstitial is served with 403; browsers still run the JS and pass.

CI/CD

A Forgejo Actions runner (herbesvertes-build, label nas-build) on the herbesvertes build box watches this repo. On a push that touches rebrand/**, Dockerfile.ekaii, VERSION, policy.yaml, or the workflow, .forgejo/workflows/build.yml:

  1. Build — clones Anubis v1.25.0 + builds nopasaran:$(cat VERSION) via the host docker socket.
  2. Smoke (test/ci-smoke.mjs, gate) — boots a throwaway container and asserts: minimal frosted render, branded title, no footer/leaks, the renamed schema (q/z/rb — stock solvers defeated), and a full PoW solve → 302. A red smoke aborts before any deploy.
  3. Deploydocker save | ssh nas docker load, bumps the compose image, recreates the nopasaran service on the NAS, and verifies it's live and healthy.

To cut a release: bump VERSION and push. That's the whole flow — no manual build/load/deploy. workflow_dispatch (with deploy) re-runs it on demand. The runner uses a dedicated herbesvertes→NAS deploy key (mounted into jobs at /opt/nas_deploy); no image registry is involved (build host → NAS is a single LAN docker load).