fix: bound ipns cache-control to record eol#1166
Conversation
max-age plus stale-while-revalidate could extend past an IPNS record's EOL, letting caches serve an expired record that then fails validation. Cap max-age to the remaining validity and size the stale window to whatever validity is left, so max-age+stale never crosses EOL. An already-expired record is returned with Cache-Control: no-store.
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #1166 +/- ##
==========================================
+ Coverage 63.39% 63.45% +0.05%
==========================================
Files 268 269 +1
Lines 26967 27002 +35
==========================================
+ Hits 17096 17133 +37
+ Misses 8147 8142 -5
- Partials 1724 1727 +3
... and 6 files with indirect coverage changes 🚀 New features to boost your workflow:
|
A record's TTL is read from CBOR without a sign check, so a negative value would make min(ttl, remainingValidity) emit a malformed Cache-Control: max-age=-N. Floor ttl at zero before capping.
The raw IPNS record handler set max-age straight from record.TTL(), bypassing the resolver's EOL clamp, so a cache could reuse the record past its signature expiry; a negative TTL also yielded a negative max-age. Cap max-age to the remaining validity and floor it at zero.
calculateBestTTL clamped a record TTL down to the remaining EOL validity but never floored a negative record TTL, so a malformed record with a future EOL leaked a negative duration through namesys.Result.TTL. Floor the record TTL at zero.
GET /routing/v1/ipns/{name} assumed the default 48h record lifetime
for any record whose ValidityType was not EOL (or could not be
parsed), advertising a stale-serving window for a record whose
expiration is unknown. Treat such records as uncacheable instead.
On a cache hit the resolver returned the record's original TTL, so a hit late in the cache window re-advertised the full TTL and could let a downstream cache outlive the record's EOL. Return the cache entry's remaining lifetime instead, which is already bounded by the EOL.
A negative TTL was stored verbatim: the CBOR field went negative and the v1 uint64 protobuf field underflowed to a huge value. NewRecord now floors the TTL at zero, and Validate rejects a record whose CBOR TTL is negative. Tests that need a genuinely negative TTL build records at the wire level via internal/ipnstest, since NewRecord no longer mints them.
|
Hey @gammazero, heads-up before this lands: it grew quite a bit since your approval (which was on just the initial
I smoke-tested all of it in ipfs/kubo#11349 (bumps kubo to this branch's tip, CI green) and used it there to add user-facing Could you give the post-approval commits a final look? If they look good to you, this is ready to merge and cut a boxo release, which is what lets someguy/delegated-ipfs.dev pick it up and clear the service-worker-gateway 500s (ipfs/service-worker-gateway#1072). Thanks for the earlier review! |
Problem
GET /routing/v1/ipns/{name}advertised a cache window that outlived the record:max-agewas the record TTL andstale-while-revalidate/stale-if-errorcovered the full remaining validity, so the stale-serving window ran past the record's EOL by up to onemax-age. A cache (the CDN in front ofdelegated-ipfs.dev, a browser, or a Helia HTTP cache) then served a record whose signature had already expired, which validating clients reject, surfacing as sporadic 500s in the service-worker gateway. The other IPNS surfaces had the same gap, and a malformed negative TTL propagated unchecked, even underflowing the v1uint64TTL field.Fix
routing/http/server: capmax-ageto the record's remaining validity and size the stale window to the validity left after it, somax-age+stale never crosses EOL; returnno-storefor a record that is expired or carries an unrecognizedValidityType.gateway: the raw record endpoint (GET /ipns/{name}?format=ipns-record) capsmax-ageto the remaining validity.namesys: the resolver floors a negative record TTL, and a cache hit reports the entry's remaining lifetime instead of the original TTL.ipns:NewRecordfloors a negative TTL (also closing the v1uint64underflow) andValidaterejects a record carrying one.No cache reuses an IPNS response past the record's EOL, and a negative TTL can no longer surface through any IPNS API.