Repository navigation
perf(AUTO-1063): skip parsing hostnames and cache verdicts in isAddressRestricted - #1571
Conversation
…ssRestricted A hostname made ipaddr.process() throw on every request (~7 us for the stack trace), and an IPv4 address threw once per IPv6 range in the policy. Return early for hosts that cannot be an IP literal, and remember verdicts per host string on networkOptions.restrictedVerdicts (bounded at 1,000 entries). Verdicts are unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## develop #1571 +/- ##
============================================
- Coverage 77.61% 46.94% -30.68%
============================================
Files 51 51
Lines 4361 4375 +14
Branches 1234 1238 +4
============================================
- Hits 3385 2054 -1331
- Misses 732 2156 +1424
+ Partials 244 165 -79
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| * @returns {Boolean} the verdict, so callers can return it directly | ||
| */ | ||
| rememberAddressVerdict = function (verdicts, host, verdict) { | ||
| verdicts.size >= MAX_CACHED_ADDRESS_VERDICTS && verdicts.clear(); |
There was a problem hiding this comment.
[P2] Clearing the entire cache on overflow evicts frequently used IP verdicts, bringing back parsing and CIDR matching for active destinations. AUTO-1063 calls for a bounded LRU: evict the least recently used entry and refresh recency on hits.
There was a problem hiding this comment.
Done in a6bfb79. Hits now re-insert the entry (most recently used) and overflow evicts the oldest key instead of clearing the cache. Costs about 33 ns more per hit against the ~7 µs throw it replaces. Added a test that a touched entry survives overflow and the untouched oldest one is evicted.
…t instead of clearing the cache Refresh recency on a hit and evict the oldest entry on overflow, so frequently used verdicts survive when the cache fills. Co-Authored-By: Claude <noreply@anthropic.com>
…v4 form ipaddr.js accepts Generated-corpus property test: every colon-free string ipaddr.js accepts must reach the parser, so an ipaddr.js bump that widens its IPv4 grammar (e.g. 0o octal) fails CI instead of opening a bypass. Also corrects the pre-check and verdict-cache comments. Co-Authored-By: Claude <noreply@anthropic.com>
Context
AUTO-1063: per-VU throughput of our cloud performance runs dropped after the restricted-address policy was turned on (#1548 added CIDR support; the performance library started passing a 19-entry policy). On Graviton (c6g) at 25,000 VUs, turning the policy on costs about 6.3% per-VU throughput on a 100 ms endpoint and 6.9% on a 50 ms endpoint (policy off vs on, load test below). A slow endpoint (1,000 ms) shows no difference.
isAddressRestrictedruns on every request for the URL's hostname (requester.js:434), and again on each newly resolved address inside the DNS lookup override (core.js:309,core.js:322). I measured each call separately on our load workers: switching off only the hostname check recovers almost all of it (6.0 of 6.4 points in that experiment); switching off only the resolved-address check changes nothing measurable (it runs once per new connection, and connections are kept alive).The cost is
ipaddr.jsthrowing:ipaddr.process()throws "neither IPv6 nor IPv4 format", and the catch at the bottom of the function turns that intofalse. Building the error (stack trace) is about 7 µs, on every request.addr.match()throws on each IPv6 range in the policy ("cannot match ipv4 address with non-ipv4 one"). With 3 IPv6 ranges a public IPv4 throws 3 times, about 9.4 µs per call.Summary of the change
All inside
isAddressRestrictedinlib/requester/core.js. The policy, the call sites, the DNS-time check and the redirect check are unchanged.:and a character outside[0-9a-fx.]returnsfalsewithout parsing. Every IPv4 formipaddr.js2.x accepts (dotted decimal, octal,0xhex, short and single-number forms) uses only those characters, andipaddr.jsrejects every colon-free IPv6 form (its IPv6 regex matches strings likedeadbeefand1%eth0, but its part-count check rejects them), so such a host can never match a CIDR entry; the parse would only throw. A generated-corpus unit test asserts this againstipaddr.js, so an upgrade that widens its IPv4 grammar (e.g.0ooctal) fails CI. It stays after the exact-match block so listed names likelocalhostare still blocked. Anything IP-shaped (0x7f.0.0.1,2130706433,fe80::1%eth0) still takes the full path, which matters because Node skips the lookup hook for canonical dotted-quad and bare IPv6 hosts (net.isIP≠ 0), so for those the hostname check is the only guard.networkOptions.restrictedVerdicts(aMapcapped at 1,000 entries, evicting the least recently used). This needs a stronger assumption than the one the function already makes forrestrictedCidrs: a cache hit also skips the exact-match check, sorestrictedAddressesmust not change at all after the first call, exact-match entries included. The cache is keyed by the exact string checked, never by where a name resolves to, so when DNS starts returning a different address for a name, the resolved-address check sees the new address string and checks it on its own (there is a test for this).Breaking changes
None for callers. Verdicts are identical for every input in the new and existing tests.
networkOptionsgains arestrictedVerdictsproperty, written lazily likerestrictedCidrs. An embedder that buildsnetworkOptionsfrom untrusted input should not let that input setrestrictedVerdicts(the same applies torestrictedCidrstoday); the performance library strips both, and I'll make that explicit in its own PR.Dependencies
None.
Test plan
New unit tests in
test/unit/requester-core.test.js(hostname pre-check and verdict cache):localhost).ipaddr.process(spy). IP-shaped strings always do.networkOptionsdoes not parse again.ipaddr.jsaccepts still reaches the parser. Patchingipaddr.jsto accept0ooctal parts makes this test fail.Suites:
node npm/test-unit.js583 passing, 10 pending, 0 failing.node npm/test-integration.js1,915 passing, 33 pending, 0 failing (includestest/integration/sanity/restricted-addresses.test.js). ESLint clean on both files.Microbenchmark (ns per call, the performance library's 19-CIDR policy, Node 24.14, median of 5 × 200k calls, same verdict before and after):
The first check of an IP literal is no faster (slightly slower, from the cache insert): its cost is the
addr.match()throws on IPv6 ranges, which only the cache avoids on repeats.Load test (our load workers, a performance library build that vendors this branch):
Per-VU requests/second, 100 ms and 50 ms endpoints, fixed profile, 5 min, one worker image with only this change versus the same image without it. 0% errors in every run.
Graviton (c6g), 25,000 VUs. Workers peak at 92-94% CPU:
AMD (c8a). Workers peak at 74-78% CPU, so there is less to win:
On c6g the fix lands within 0.6% of running with the policy switched off. At 100,000 VUs I compare total requests, because the reported requests/second uses a measurement window that varied between 284 s and 301 s.
Additional notes
develop.Generated with Claude Code