fix(mcp): pin outbound connections to IPv4 to avoid unreachable-IPv6 hangs#5798
Conversation
…hangs Root cause of the MCP 'connecting forever' / tool-discovery timeouts in production: SSRF pinning forces each outbound connection to a single resolved IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the IPv6 address first for Cloudflare-fronted dual-stack hosts (Gauge, api.exa.ai), so the connection was pinned to IPv6 — but the production app subnets have no IPv6 egress (AWS NAT gateways are IPv4-only), so the pinned IPv6 connection connects into a void and hangs until the 30s timeout. Intermittent because the resolver rotates A/AAAA order; works on retry when it happens to pick IPv4. This is why h1.1 (#5797) did not fix it (protocol-independent) and why it never reproduced locally (dev machines have IPv6 egress). Empirically verified: pinning only the IPv6 address times out; preferring the IPv4 address connects in ~600ms. Fix: at both pinned-resolution sites (validateMcpServerSsrf and validateUrlWithDNS) resolve all addresses and prefer an IPv4 one; IPv6-only hosts still pin their sole address. No signature/threading changes; SSRF validation of the pinned IP is unchanged.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview
Tests mock the multi-record lookup shape and add cases for dual-stack (IPv4 preferred) and IPv6-only pinning. Reviewed by Cursor Bugbot for commit a866409. Configure here. |
Greptile SummaryThis PR changes pinned outbound connections to prefer IPv4 while preserving IPv6-only resolution. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (2): Last reviewed commit: "chore(mcp): trim inline comments on the ..." | Re-trigger Greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a866409. Configure here.
…5799) Same class of bug fixed for MCP in #5798: SSRF pinning forces a single resolved IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the IPv6 address first for dual-stack hosts, so a database host or 1Password Connect server that is dual-stack gets pinned to IPv6 — unreachable on IPv4-only egress (AWS NAT gateways) and hangs until timeout. validateDatabaseHost and validateConnectServerUrl now resolve all addresses and prefer IPv4; IPv6-only hosts still pin their sole address. SSRF validation of the pinned IP is unchanged (the selected address is the one validated and pinned).
The real root cause (empirically confirmed)
The MCP "connecting forever" / tool-discovery 30s timeouts in production — affecting Gauge, Exa, and PlanetScale alike — were caused by IP-family pinning, not OAuth or HTTP/2 (which the earlier PRs fixed but were different layers).
The chain, every link verified against production:
createPinnedLookup), which strips Happy Eyeballs' IPv4 fallback.dns.lookup(host, { verbatim: true })returns the IPv6 address first for Cloudflare-fronted dual-stack hosts — verified:app.withgauge.com→2606:4700:…,api.exa.ai→2606:4700:….AbortError,durationMs=30002). Intermittent because the resolver rotates A/AAAA order (works on retry when it picks IPv4).This explains everything that didn't add up: why #5797 (h1.1) didn't fix it (protocol-independent), and why it never reproduced locally (dev machines have IPv6 egress).
Empirical proof (undici, pinned lookup):
Fix
At both pinned-resolution sites —
validateMcpServerSsrf(MCP) andvalidateUrlWithDNS(providers / A2A / SMTP, same latent bug) — resolve all addresses ({ all: true, verbatim: true }) and prefer an IPv4 address; IPv6-only hosts still pin their sole address. This keeps the single-IP pinning API and threading untouched (near-zero blast radius) and SSRF validation of the pinned IP is unchanged.Verified end-to-end:
app.withgauge.comandapi.exa.ainow pin their IPv4 address instead of the unreachable IPv6.Type of Change
Testing
lib/mcpsuite green (374);lib/core/securitygreen; tsc + biome clean.Note
A fuller fix (pin the validated set of all resolved IPs and let undici do Happy Eyeballs, so it's egress-agnostic) is a good follow-up, but it threads
string → string[]through ~10 security-critical sites; preferring IPv4 fixes the confirmed bug now with minimal risk.Checklist