Skip to content

fix(ext/node): bind to IPv6 wildcard for default Server.listen() to enable dual-stack#33617

Merged
littledivy merged 4 commits into
denoland:mainfrom
divybot:claude/test-http2-ip-address-host
Apr 29, 2026
Merged

fix(ext/node): bind to IPv6 wildcard for default Server.listen() to enable dual-stack#33617
littledivy merged 4 commits into
denoland:mainfrom
divybot:claude/test-http2-ip-address-host

Conversation

@divybot

@divybot divybot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables test-http2-ip-address-host in node_compat suite.

Test plan

  • cargo test --test node_compat -- test-http2-ip-address-host

divybot and others added 3 commits April 28, 2026 07:15
…nable dual-stack

Enables tests/node_compat/runner/suite/test/parallel/test-http2-ip-address-host.js

Co-authored-by: Divy Srivastava <[email protected]>
…isten()

Removes the Windows-specific IPv4-only fallback so the default
listen() binds the IPv6 wildcard on all platforms (matching Node), and
updates two unit tests that asserted IPv4-only remoteAddress/family
formats from the old behavior.

Co-authored-by: Divy Srivastava <[email protected]>

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix restores Node's default IPv6-first binding behavior that was workaround-disabled in _createServerHandle and _setupListenHandle. Both blocks now try bind6(DEFAULT_IPV6_ADDR) first and fall back to IPv4 only on failure, matching lib/internal/net.js upstream. The implementation symmetry between the two call sites is good.

But this is a behavior change for stable APIs that I want a maintainer to weigh in on before landing:

  • net.createServer().listen() (no host arg) currently binds 0.0.0.0. After this PR it binds :: and accepts IPv4 connections via dual-stack as ::ffff:127.0.0.1-style mapped addresses.
  • Same for http.createServer().listen(), http2.createSecureServer().listen(), etc.
  • User code that does if (socket.remoteAddress === "127.0.0.1") { ... } (or socket.remoteFamily === "IPv4") will silently start failing — see the test changes in tests/unit_node/http_test.ts and tests/unit_node/net_test.ts that the author had to update.
  • The original IPv4-only behavior was a deliberate workaround per the existing // TODO(@bartlomieju): differs from Node ... Forcing IPv4 as a workaround for Deno not aligning with Node on implicit binding on Windows comment, with issue #10762 as the rationale. The PR doesn't link a comment confirming the underlying Windows issue is resolved (or that bind6 reliably falls back when Windows can't dual-stack).

cc @bartlomieju — the workaround was originally yours and the Windows reliability question is the load-bearing one. Worth confirming current Windows behavior before this lands.

Holding off on autonomous APPROVE per fibibot's policy on:

  1. Breaking changes to stable Node-compat APIs (socket.remoteAddress, socket.remoteFamily).
  2. CI not yet complete (58/n passing, no failures yet but most checks queued).

One smaller observation about CI noise inline.

Comment thread ext/node/polyfills/net.ts
err = (handle as TCP).bind6(DEFAULT_IPV6_ADDR, port ?? 0, flags ?? 0);
if (err) {
handle.close();
return _createServerHandle(DEFAULT_IPV4_ADDR, port, 4, null, flags);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/safety nit on the IPv6→IPv4 fallback: when bind6 fails, the code calls handle.close() then recurses into _createServerHandle(DEFAULT_IPV4_ADDR, ...) which creates a new TCP handle. The recursion is fine and matches Node, but the recursive call can fail too — and if both binds fail, the user just gets the IPv4 error code with no indication that an IPv6 attempt happened first. Worth confirming this matches Node's behavior (Node exposes only the second attempt's error per lib/internal/net.js's lookupAndListen, so this is probably fine, just verifying).

More importantly: the PR's bind6 → fallback to bind flow assumes the IPv6 wildcard reliably accepts IPv4 connections via dual-stack on Windows. The original workaround comment (Forcing IPv4 as a workaround for Deno not aligning with Node on implicit binding on Windows) was specifically about Windows — has the underlying issue (#10762) been verified resolved on Windows debug + release? CI's test node_compat debug windows-* shards aren't in yet on this run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right to flag this - CI on Windows confirmed dual-stack does not reliably accept IPv4 connections in our environment (the tests/specs/node/http_server_connect_event and node_compat::sequential::test-net-localport.js Windows shards both reproduced connect ECONNREFUSED 127.0.0.1 against a server bound to ::).

I've restored the Windows-specific IPv4 fallback in _createServerHandle / _setupListenHandle (so non-Windows now matches Node, Windows keeps the existing IPv4-only behavior) and skipped parallel/test-http2-ip-address-host.js on Windows via tests/node_compat/config.jsonc. The two unit_node assertion changes stay because they're more permissive (accept either 127.0.0.1 or ::ffff:127.0.0.1) and work on both code paths.

CI confirmed Windows dual-stack does not reliably accept IPv4
connections (server bound to :: refuses 127.0.0.1 clients with
ECONNREFUSED). Restore the Windows-specific IPv4 fallback and skip
parallel/test-http2-ip-address-host.js on Windows.

Co-authored-by: Divy Srivastava <[email protected]>

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on the delta. The Windows-IPv4-only carve-out is the minimal fix for the windows-x86_64 CI failure I flagged before. Verified:

  • isWindows already imported at ext/node/polyfills/net.ts:105, no new import needed.
  • Both _createServerHandle and _setupListenHandle get the same Windows guard (mirror logic — important for Server.listen() paths going through either function).
  • test-http2-ip-address-host.js is correctly Windows-disabled with a one-line reason; the windows: false shape matches existing config entries.
  • Linux/macOS path is unchanged — still IPv6 dual-stack with IPv4 fallback, matching Node.

CI: 134/134 green, all test node_compat debug shards including windows-x86_64 pass.

One thing worth flagging for follow-up (not blocking): on Windows, IPV6_V6ONLY defaults to TRUE, which is why an IPv6-bound socket there doesn't accept IPv4 connections. Node solves this by clearing IPV6_V6ONLY on its dual-stack sockets — a future PR setting that socket option in the Deno bind path would let Windows match Node's "single-stack-bound-with-IPv4-fallback" default rather than diverging to IPv4-only. The new comment ("in our environment") is honest about this being a workaround, but it's worth a tracking issue if there isn't one already.

cc @bartlomieju on the Windows divergence — the behavior change is small but it's a default-listen-address difference from Node on a stable API.

@littledivy littledivy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, windows divergence fine until #10762 is fixed

@littledivy
littledivy merged commit 1943d24 into denoland:main Apr 29, 2026
136 checks passed
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.

3 participants