Skip to content

Release 0.35.0 - #29

Merged
Dim145 merged 2 commits into
mainfrom
release/0.35.0
Sep 3, 2026
Merged

Dim145 merged 2 commits into
mainfrom
release/0.35.0

Conversation

@Dim145

@Dim145 Dim145 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

101 commits since 0.34.0 — 20 features, 68 fixes, the rest docs, tests, CI
and two performance changes. Most of it came out of an audit pass over the
four apps, where the findings that mattered came from probing the running
stack, not from static analysis
: semgrep, trivy, gitleaks, gosec, hadolint,
actionlint, zizmor, govulncheck and osv-scanner were clean or already
arbitrated, while calling 131 GET and 90 write routes without a session — and
then with another member's session — was what turned up the real leak.

Security

  • safeFetch handed the caller's credentials to whoever asked for a
    redirect.
    The SSRF guard re-validated every hop, so no private range could
    be reached, but the caller's init was spread into every fetch — headers
    included. A public target answering 302 Location: https://elsewhere
    received the sixteen member-chosen webhook headers, the body HMAC, the ntfy
    Authorization or the object store's SigV4 signature. Fixed with an
    allowlist rather than the standard denylist, because the callers here set
    their own credential headers. A 307/308 to another origin carrying a body
    is now refused outright rather than replayed.
  • The freeleech pool named its top members to anonymous callers, with
    internal ids. The anonymous path existed for one field; the contributor list
    had come along with it.
  • The Caddy credential scrub in the access log had never matched a field
    uri instead of request>uri — so passkeys had been written in cleartext
    since it was added, without a warning.
  • A banned member kept the account routes under /api/auth; an IP ban locked
    staff out of the very route that lifts it; the API served a CSP written for
    the web application.
  • The Torznab feed published the announce passkey to read-key holders, and a
    block a member could lift by rotating their own key.
  • encrypt was an unbounded oracle against the panic password, and a
    twelve-character password was derived at Node's defaults.
  • One address seen on five days read as five different places in the login
    history.
  • Postgres trusted the network in the reference compose, and Caddy had no body
    limit.

Performance

  • Byte credits no longer ride the announce. A member seeding 70 torrents
    was updating their own row 70 times per interval, and the increments are
    commutative. Measured over five minutes at 1 964 credited announces/s, 50 000
    members, steady state: WAL 113–125 MB → 45.8 MB at the 60 s default and
    7.5 MB at 300 s; statements 1.00 per announce → 0.038. Table and index sizes
    unchanged. Flushed in SMALL chunks, because a single-block flush defeats HOT
    pruning and writes more WAL than the individual writes it replaces.
  • Migration 0071 was measured wrong; 0072 corrects it. Its benchmark ran
    100 000 updates inside one DO block — one transaction — so nothing could
    prune and fillfactor looked guilty. Remeasured with one transaction per
    announce, hnr_tracking reaches full HOT at 95 for +0.5% of table, where 70
    cost +35% for the same result.
  • The swarm-cache ceiling counted traffic rather than memory; a hundred
    concurrent peer ids could reach a hundred GiB/s of credit; an unauthenticated
    scrape could reach Postgres.

Features

Members — a copy of your own record and what it deliberately leaves out;
three keys instead of one doing three jobs; alerts on saved searches; "sign out
everywhere"; where an account has been signed in from; drafts kept until sent.

Tracker — BEP 52: the second infohash a hybrid torrent announces under; a
partial seed is not a seed; an operator can close UDP scrape (BEP 15 has no
field for authentication in a scrape request, so a private tracker closes the
door rather than guarding it).

Torznab / integrations — a Prowlarr definition generated per instance; the
seeding rules a client was never told, and rates that were a lie; one line in
an IRC channel and the parser for it in the same breath.

Staff — who vouched for them and who they let in; an audit register across
the whole console; the moderation queue says which of these are yours; a
freeleech you can put on a single torrent.

Site — a statistics page, the site looking at itself; installable as a PWA,
with an icon size nobody made up; three things a catalogue needs when it gets
old.

Interface and accessibility

An interface pass produced five hundred findings; four systemic faults were
behind most of them
and were fixed at the root. Since then: 49 form controls
had no accessible name, then 24 more that a first crude count had put at ~300 —
the difference was three classes of false positive, now documented in the test
that guards the number. A modal with no focus trap and a group row only a mouse
could open. A sanitiser that passed a phishing link and ate legitimate
attributes. Two locales hardcoded in a bilingual site, an error page in
English, numbers formatted in the wrong locale. Two pages that shipped the
server's clock and corrected it on hydration. document.write on the
recovery-codes screen. Three pages rendering bare native buttons.

Correctness

  • A failed seed-time write blocked its own retry for 900 seconds.
  • Closing the UDP socket stopped the read loop but not the goroutines writing
    to Postgres; shutdown now drains them.
  • A topic author could delete everyone else's replies.
  • A takedown left the catalogue, the page and the file untouched.
  • The IRC queue paced nothing and two thirds of announces never left.
  • stop() then start() left an orphan message stream delivering twice.
  • A relay race unsubscribed a channel for good, and the drain signalled nobody.
  • A post from twelve minutes ago read as two hours old — db.execute silently
    drops the timestamp parser that db.select honours.
  • 40 routes where a malformed query parameter answered 500 instead of a
    readable 400.
  • A case-different username was a second account, and the foreign keys had no
    index.
  • One proof of work could be spent twice.
  • The dashboard's tracker light was a constant, not a status.
  • Saved searches rebuilt their link with the API's parameter names, so the
    saved URL never reopened the search.
  • The integration suite never connected Redis, so eleven failures per CI
    job were invisible — and worse, the tests were not exercising what they
    appeared to: account erasure passed while its Redis half did nothing at all.

Operators

Two new settings, both on the tracker, both optional:

Variable Default What it does
TRACKER_STATS_FLUSH_INTERVAL 60s Byte-credit batching window. 0s restores the per-announce write. 300s divides WAL by 15 at the cost of five minutes of staleness on the counters a member sees.
TRACKER_STATS_FLUSH_CHUNK 10 Members written per transaction during a flush. Do not raise without measuring — past a few dozen, a flush writes more WAL than what it replaces.

TRACKER_UDP_SCRAPE_ENABLED shipped in 0.34.0's successor work but was never
in the env reference or the Helm chart; both are corrected here
(tracker.udp.scrapeEnabled).

15 migrations (0058–0072), applied at boot. Verified to apply to an empty
database: 204 tables, 1 672 columns, 578 indexes.

Shutdown grace period. The tracker now flushes byte credits (up to 10 s)
and drains in-flight UDP datagrams (up to 5 s) before the existing HnR drain
(8 s) — 33 s worst case. Compose gained stop_grace_period: 45s; the chart's
terminationGracePeriodSeconds moved 30 → 45. Docker's 10 s default was
already cutting the HnR drain short before this release.

A NetworkPolicy is now shipped with the chart.

After upgrading

Migration 0072 sets fillfactor on hnr_tracking but deliberately rewrites
nothing — a boot-time migration must not take an exclusive lock on a table in
the announce path. To realise it on existing pages:

pg_repack --no-order --table public.hnr_tracking -d trackarr

Online, no maintenance window. VACUUM FULL public.hnr_tracking is the
alternative and takes an ACCESS EXCLUSIVE lock.

Verification

Every CI gate, plus what CI cannot reach. The behavioural claims above were
checked against a running compiled stack rather than read off the source: the
Caddy scrub with a canary, the forum cascade, session revocation across two
devices, the credit-budget clamp (2 000 GiB claimed → 68 GiB credited), the UDP
scrape switch in both positions, the byte-credit batching against a real
tracker with real announces, and the redirect header-stripping against real
undici with two servers on two origins.

🤖 Generated with Claude Code

Dim145 and others added 2 commits September 3, 2026 18:11
…hart never exposed it

The switch landed with `feat(tracker): let an operator close UDP scrape`.
It reached `config.go`, `docker-compose.prod.yml` and the UDP guide — and
neither `doc/reference/env.md`, which is where an operator looks for the
list, nor the Helm chart, where a Kubernetes operator has no other way to
set it.

Found while assembling the release notes: the env reference went from 91
variables to 93 between 0.34.0 and now, and this was not one of them.

`tracker.udp.scrapeEnabled` in values.yaml, wired in the tracker
template. Verified in the rendered chart rather than in the source —
the first render came back without it, which was the bind-mount cache
serving a stale values.yaml, not a templating mistake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Dim145
Dim145 merged commit baa4297 into main Sep 3, 2026
12 checks passed
@github-actions
github-actions Bot deleted the release/0.35.0 branch September 3, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant