Skip to content

perf(AUTO-1063): skip parsing hostnames and cache verdicts in isAddressRestricted - #1571

Open
Kart03 wants to merge 1 commit into
developfrom
claude/auto-1063-rca-05deed
Open

Kart03 wants to merge 1 commit into
developfrom
claude/auto-1063-rca-05deed

Conversation

@Kart03

@Kart03 Kart03 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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 a 100 ms endpoint at 25,000 VUs it costs about 6.4% per-VU throughput, and 7.4% on a 50 ms endpoint. A slow endpoint (1,000 ms) shows no difference.

isAddressRestricted runs 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 6.0 of the 6.4 points; 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.js throwing:

  • For a hostname, ipaddr.process() throws "neither IPv6 nor IPv4 format", and the catch at the bottom of the function turns that into false. Building the error (stack trace) is about 7 µs, on every request.
  • For an IPv4 address, 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 isAddressRestricted in lib/requester/core.js. The policy, the call sites, the DNS-time check and the redirect check are unchanged.

  1. Hostname pre-check. After the exact-match lookup, a host with no : and a character outside [0-9a-fx.] returns false without parsing. Every IPv4 form ipaddr.js 2.x accepts (dotted decimal, octal, 0x hex, short and single-number forms) uses only those characters, and every IPv6 literal contains :, so such a host can never match a CIDR entry; the parse would only throw. It stays after the exact-match block so listed names like localhost are still blocked. Anything IP-shaped (0x7f.0.0.1, 2130706433, fe80::1%eth0) still takes the full path, which matters because Node never calls the lookup hook for numeric hosts, so the hostname check is the only guard there.
  2. Verdict cache. Verdicts are remembered per lower-cased host string on networkOptions.restrictedVerdicts (a Map, cleared when it reaches 1,000 entries). This relies on the same assumption the function already makes for restrictedCidrs: restrictedAddresses does not change after the first call. 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. networkOptions gains a restrictedVerdicts property, written lazily like restrictedCidrs. An embedder that builds networkOptions from untrusted input should not let that input set restrictedVerdicts (the same applies to restrictedCidrs today); 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):

    • 21 blocked and 15 allowed inputs keep the verdicts recorded from the current implementation (hex/octal/short IPv4, IPv4-mapped and IPv4-compatible IPv6, NAT64, 6to4, bracketed IPv6, zone IDs, uppercase, trailing dots, names, localhost).
    • Hostnames never reach ipaddr.process (spy). IP-shaped strings always do.
    • A repeat check with the same networkOptions does not parse again.
    • DNS rebinding: name allowed, first address allowed, then a new blocked address is blocked.
    • Cache stays at or under 1,000 entries after 1,500 distinct addresses and verdicts stay correct.
    • Before the change, 3 of these 10 fail (no-parse, no-reparse, bounded cache) and 7 pass; after, all pass.
  • Suites: node npm/test-unit.js 582 passing, 10 pending, 0 failing. node npm/test-integration.js 1,915 passing, 33 pending, 0 failing (includes test/integration/sanity/restricted-addresses.test.js). ESLint clean on both files.

  • Microbenchmark (one call, 19-entry policy, Node 24, same verdict before and after):

    Input Before After, repeat After, first call
    Hostname 6,988 ns 108 ns 180 ns
    Public IPv4 9,799 ns 22 ns 8,055 ns
    Private IPv4 6,490 ns 13 ns 5,455 ns
    Loopback 326 ns 14 ns 365 ns
  • 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:

    Endpoint Without fix With fix Policy off With fix vs without
    100 ms 1.039, 1.031 1.099, 1.098 1.105 +6.1%
    50 ms 1.058, 1.057 1.131, 1.142 1.136 +7.5%

    AMD (c8a). Workers peak at 74-78% CPU, so there is less to win:

    Endpoint VUs Without fix With fix Change
    100 ms 25,000 3.430 3.443 +0.4%
    50 ms 25,000 3.890 3.964 +1.9%
    100 ms 100,000 2 pairs, total requests 101.6M / 99.0M and 98.2M / 98.1M no measurable difference
    50 ms 100,000 3.712 3.865 +4.1%

    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

  • Base is develop.
  • This touches an SSRF guard, so I'd like a security review in addition to the usual one. The parts worth reading closely are the pre-check condition and its placement after the exact-match block.
  • Not in this PR: matching an address only against ranges of its own family, which would remove the remaining throws for the first check of an IP. The cache already makes repeats cheap, and the load test (hostname targets) shows the fix recovers the full cost, so I'll only add it if IP-literal targets turn out to matter.

Generated with Claude Code

…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

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.67%. Comparing base (4e40645) to head (f8d3a34).
⚠️ Report is 3 commits behind head on develop.

❌ Your project check has failed because the head coverage (34.15%) is below the target coverage (35.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1571      +/-   ##
===========================================
+ Coverage    77.61%   77.67%   +0.05%     
===========================================
  Files           51       51              
  Lines         4361     4371      +10     
  Branches      1234     1238       +4     
===========================================
+ Hits          3385     3395      +10     
  Misses         732      732              
  Partials       244      244              
Flag Coverage Δ
integration 68.88% <85.71%> (+0.02%) ⬆️
legacy 34.15% <21.42%> (-0.01%) ⬇️
unit 46.90% <100.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Kart03
Kart03 marked this pull request as ready for review October 23, 2026 07:18

This branch has not been deployed

No deployments
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