- PHP 76.2%
- Shell 21.6%
- Dockerfile 2.2%
The LinuxServer nginx build buffers the request body before PHP runs, despite fastcgi_request_buffering off, so php://input timing read ~0 (bogus multi-Gbps). Instead ul.php emits a meta-refresh page with a __NRT__ placeholder that nginx rewrites to $request_time via sub_filter; result.php divides uploaded bytes by it. Validated on a NAS scratch stack: upload tracks the throttle (19 Mbps @ ~2.4 MB/s, 95 Mbps @ ~11.8 MB/s), download ~38-40 Mbps @ 5 MiB/s throttle, onion served end-to-end over Tor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| nginx | ||
| nojs | ||
| tor | ||
| .gitignore | ||
| deploy.sh | ||
| docker-compose.yml | ||
| README.md | ||
librespeed-nojs
A JavaScript-free, Tor-friendly overlay for LibreSpeed running on the LinuxServer.io image.
Browsers with scripting disabled — Tor Browser at the "Safest" level, NoScript
users — get bounced from / to a /nojs/ test that works with pure HTML. The
JavaScript test at / is untouched.
Why a no-JS version is inherently limited
Measuring throughput means counting bytes against a clock on the client, and the only clock + byte counter a web page has is JavaScript. Plain HTML has none. So this overlay is an honest, degraded fallback, not a replacement:
| Metric | No-JS approach | Quality |
|---|---|---|
| Download | Browser fetches a large incompressible payload; a trailing <meta refresh> fires only once the whole payload has arrived. Server brackets the transfer with two request timestamps and subtracts a one-hop latency baseline. |
Estimate (includes overhead) |
| Upload | A form POSTs an incompressible body; nginx records the request duration ($request_time) and injects it into the response via sub_filter; result.php divides bytes by that time. |
Good |
| Ping / jitter | Not possible without a client clock. | Only a rough round-trip latency is shown |
How the flow works
All state is carried in the URL as an HMAC-signed token — no cookies, no server-side sessions (deliberate, for Tor users with restricted storage).
/nojs/ pick a profile (Quick / Standard / Large)
-> start.php stamp t0, 302 ->
-> calib.php stamp t1; base = t1-t0 (one empty hop ≈ 1 RTT), 302 ->
-> dl.php stamp t_d0; stream N bytes of random base64 in a <head>
comment; trailing <meta refresh> ->
-> dl_done.php stamp t_d1; download ≈ (t_d1-t_d0) - base; show upload form ->
-> ul.php count body bytes; emit meta-refresh page with __NRT__ ->
-> result.php download (est.) / upload (bytes ÷ nginx $request_time) / latency
The download payload is random base64: gzip and Tor compression can't shrink
it, and base64 never contains - or <, so it can't break out of the HTML
comment. nginx gzip is also disabled on dl.php.
For upload, PHP can't time the transfer because this nginx build buffers the
request body before the script runs (despite fastcgi_request_buffering off).
Instead, nginx's sub_filter rewrites the __NRT__ placeholder in ul.php's
response to $request_time (the true request duration), and result.php
divides the uploaded byte count by it. If sub_filter is unavailable, upload is
reported as not measured rather than wrong.
Layout
nojs/ PHP overlay (lives in the persistent /config volume)
_lib.php signed tokens, formatting, page chrome
index/start/calib/dl/dl_done/ul/result.php
.user.ini raises post_max_size, disables eager POST reading
nginx/nojs.conf nginx location overlay (root /config, buffering/gzip tuning)
tor/ alpine+tor onion-service sidecar (built locally, auditable)
deploy.sh idempotent installer into a /config volume + noscript injector
docker-compose.yml librespeed + tor onion sidecar
Deploy
# 1. Bring up the stack (seeds ./config from the image on first run).
NOJS_SECRET=$(openssl rand -hex 32) docker compose up -d
# 2. Install the overlay into the config volume (re-run after image updates).
./deploy.sh ./config librespeed
# 3. (Tor) read the generated onion address, then re-run deploy.sh with it so
# clearnet responses advertise it via the Onion-Location header.
docker exec librespeed-tor cat /var/lib/tor/hs_speedtest/hostname
./deploy.sh ./config librespeed <that-hostname-without-.onion>
deploy.sh is reversible: it backs up index.html and the nginx site-conf
before editing, and the overlay block is fenced by
# >>> librespeed-nojs >>> markers.
Notes for Tor
- Over Tor you measure your circuit's throughput (3 relays + the onion path), not your own link. Use the Quick profile.
- Served as an onion service, the client's IP is never exposed to the server, so the results page reports "IP hidden" instead of an address.
- The
.onionaddress is persisted in thetor-datavolume; back it up if you want it to survive a volume reset.
Upstream
Based on LibreSpeed 6.1.0 (LGPL-3.0). This overlay adds only new files plus a
fenced nginx block and a single <noscript> line; it changes none of the
upstream JavaScript test.