- Python 77.9%
- JavaScript 22.1%
|
All checks were successful
build-and-deploy / build (push) Successful in 14s
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> |
||
|---|---|---|
| .forgejo/workflows | ||
| rebrand | ||
| test | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile.ekaii | ||
| nginx-fanout.conf | ||
| policy.yaml | ||
| README.md | ||
| VERSION | ||
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:8081 → anubis-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_challenge→nop_c,anubis_version→nop_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 withmessage "<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 readschallenge.randomData/rules.difficultyunder{challenge, rules}) gets aKeyError, while a nopasaran-aware client reads the renamed fields:upstream nopasaran where rules(wrapper)qweb/index.gochallenge(wrapper)zweb/index.gorandomDatarblib/challenge/challenge.goidklib/challenge/challenge.goissuedAttslib/challenge/challenge.gomethodmdlib/challenge/challenge.gometadatamxlib/challenge/challenge.gopolicyRuleHashphlib/challenge/challenge.godifficulty(Challenge)v2lib/challenge/challenge.gospentsplib/challenge/challenge.goweb/js/main.tsis 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.ChallengeRulestags (algorithm/difficulty/report_as) — that's theqpayload the client reads;rules.difficultydrives 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:
- parses
nop_c, asserts the renamed schema (q/z/rb), - shows a stock solver hitting
KeyError: 'challenge'(defeated), - solves the PoW (
sha256(rb + nonce),difficultyleading hex zeros), callspass-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-Iprequired: withuse-remote-address: false, the policy needsX-Real-Ip/X-Forwarded-Forfrom the proxy; a direct curl without it → 500[misconfiguration]. Traefik sets it in prod.- Testing cookies off-host:
COOKIE_DYNAMIC_DOMAIN=truescopes the cookie to.ekaii.fr, so a cookiejar keyed on127.0.0.1drops it ("user has cookies disabled"). ReplaySet-Cookiemanually, or test against the real host. CHALLENGE: 403is intentional (policystatus_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:
- Build — clones Anubis v1.25.0 + builds
nopasaran:$(cat VERSION)via the host docker socket. - 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. - Deploy —
docker save | ssh nas docker load, bumps the compose image, recreates thenopasaranservice 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).