Skip to content

test(tls): bind the Bun.connect hostname-verify listener to 127.0.0.1 (host-dependent ECONNREFUSED) - #36543

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/2b449d76/tls-hostname-verify-test-bind-ipv4
Jul 31, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
farm/2b449d76/tls-hostname-verify-test-bind-ipv4

Conversation

@robobun

@robobun robobun commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

What

Bun.connect TLS hostname verification > reports authorized=false when a CA-trusted cert does not match the connected hostname in test/js/node/tls/node-tls-connect-hostname-verification.test.ts fails with ECONNREFUSED on hosts whose only configured IPv6 address is loopback (e.g. a container with ::1 localhost in /etc/hosts and no global IPv6):

error: Failed to connect
 syscall: "connect",
   errno: -111,
    code: "ECONNREFUSED"
      at async <anonymous> (test/js/node/tls/node-tls-connect-hostname-verification.test.ts:211:35)

Why this is not a runtime bug

  • Bun.listen({hostname: "localhost"}) resolves via getaddrinfo with AI_PASSIVE only (no AI_ADDRCONFIG, bsd.c:bsd_create_listen_socket) and binds the first result, ::1.
  • Bun.connect({hostname: "localhost"}) resolves via Bun's DNS layer, which passes AI_ADDRCONFIG on Unix (dns.rs:get_hints). On a host with no non-loopback IPv6 address, AI_ADDRCONFIG filters ::1, leaving only 127.0.0.1. Happy-eyeballs runs but never sees ::1.

Node v26.3.0 behaves identically:

$ node -e 'const net=require("net");const s=net.createServer();s.listen(0,"localhost",()=>{console.log(s.address());net.connect({port:s.address().port,host:"localhost",autoSelectFamily:true}).on("connect",()=>console.log("ok")).on("error",e=>{console.log(e.code);s.close()})});'
{ address: '::1', family: 'IPv6', port: 41315 }
ECONNREFUSED

Fix

Bind the listener to 127.0.0.1. The client still connects to the name "localhost", so the assertion this test exists for (that Bun.connect verifies the presented certificate against its hostname option when no serverName is given) is unchanged: the certificate is for CN=agent1, not localhost, and must remain unauthorized. Connecting to "localhost" reaches 127.0.0.1 on every host configuration that maps localhost to 127.0.0.1 at all.

Same root cause as #35160 (different test).

Verification

On a host with ::1 localhost first and no global IPv6:

before:  7 pass  1 fail (ECONNREFUSED)
after:   8 pass  0 fail

Test-only change.


[stamp-90s] gate passed · iteration 0 · 1 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/node/tls/node-tls-connect-hostname-verification.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/node/tls/node-tls-connect-hostname-verification.test.ts
bun test v1.4.0 (f581bdc26)

test/js/node/tls/node-tls-connect-hostname-verification.test.ts:
(pass) tls.connect hostname verification without explicit servername > rejects an IP address as options.servername [152.06ms]
(pass) tls.connect hostname verification without explicit servername > rejects a CA-trusted cert whose CN does not match host [736.76ms]
(pass) tls.connect hostname verification without explicit servername > reports authorized=false on hostname mismatch with rejectUnauthorized=false [191.79ms]
(pass) tls.connect hostname verification without explicit servername > invokes checkServerIdentity with host when servername is omitted [86.64ms]
(pass) Bun.connect TLS altname mismatch reason > quotes and escapes a DNS altname containing a comma [61.04ms]
(pass) Bun.connect TLS altname mismatch reason > quotes and escapes a DNS altname containing double quotes [33.60ms]
(pass) Bun.connect TLS altname mismatch reason > quotes and escapes a DNS altname containing a non-ASCII byte [34.66ms]
(pass) Bun.connect TLS hostname verification > reports authorized=false when a CA-trusted cert does not match the connected hostname [51.60ms]

 8 pass
 0 fail
Ran 8 tests across 1 file. [4.45s]
Exit: 0
diff hotspot
test/js/node/tls/node-tls-connect-hostname-verification.test.ts | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
…node/tls/node-tls-connect-hostname-verification.test.ts      1      1      0

@claude claude Bot 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 — one-line test de-flake that binds the listener to 127.0.0.1 while the client still dials "localhost", so the hostname-verification assertion is unchanged.

What was reviewed:

  • Confirmed the client's hostname: "localhost" (mismatch) and serverName: "agent1" (match) paths are untouched, so the property under test is preserved.
  • Checked that altnameMismatchReason in the same file already binds Bun.listen to 127.0.0.1 — this brings the sibling test in line.
  • The added comment documents the AI_ADDRCONFIG listen/connect asymmetry, which is the non-obvious "why".
Extended reasoning...

Overview

Single-line functional change in test/js/node/tls/node-tls-connect-hostname-verification.test.ts: the Bun.listen bind hostname goes from "localhost" to "127.0.0.1", plus a six-line comment explaining the host-dependent ECONNREFUSED. No production code is touched.

Security risks

None. Test-only; the change narrows a bind address to loopback and does not alter TLS validation, trust anchors, or any runtime path.

Level of scrutiny

Low. This is a targeted test de-flake with a well-understood root cause (listen resolves without AI_ADDRCONFIG, connect resolves with it, so on IPv6-loopback-only hosts the listener lands on ::1 while the client only tries 127.0.0.1). The PR cites #35160 as the same class of fix, and the altnameMismatchReason helper a few lines up in this file already uses hostname: "127.0.0.1" for its Bun.listen call, so the change also matches local convention.

Other factors

The repo's de-flake rule is "keep asserting the property the original assertion protected." That holds here: both Bun.connect calls still pass hostname: "localhost", so the cert-CN-vs-connected-hostname mismatch (and the serverName: "agent1" positive control) are exercised exactly as before. The added comment is durable "why" content rather than narration. No prior reviews or outstanding comments on the PR.

Comment thread test/js/node/tls/node-tls-connect-hostname-verification.test.ts
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e179f94d-dff7-4dee-9a42-00907dbd3b7d

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec728d and f581bdc.

📒 Files selected for processing (1)
  • test/js/node/tls/node-tls-connect-hostname-verification.test.ts

Comment @coderabbitai help to get the list of available commands.

The 'reports authorized=false when a CA-trusted cert does not match the
connected hostname' test fails with ECONNREFUSED on hosts where the only
configured IPv6 address is loopback. Bun.listen resolves the bind host
without AI_ADDRCONFIG and picks ::1, while Bun.connect resolves with
AI_ADDRCONFIG which filters ::1 on such hosts, so the client dials
127.0.0.1 and is refused. Node's net.connect behaves the same way, so
this is not a runtime divergence.

Bind the listener to 127.0.0.1; the client still connects to the name
'localhost' so the hostname-derived certificate check this test covers
is unchanged.
@robobun
robobun force-pushed the farm/2b449d76/tls-hostname-verify-test-bind-ipv4 branch from 06a16b6 to f581bdc Compare July 31, 2026 08:18

@claude claude Bot 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 — one-line test de-flake that binds the listener to 127.0.0.1 while keeping the client on "localhost", so the hostname-vs-CN assertion is preserved.

What was reviewed:

  • Confirmed the test's invariant is unchanged: client still connects to "localhost" (DNS-name path), cert is CN=agent1, so ERR_TLS_CERT_ALTNAME_INVALID is still exercised.
  • Matches the existing altnameMismatchReason helper in the same file, which already binds to 127.0.0.1; withServer (for the tls.connect cases) listens on all interfaces so is unaffected.
  • Jarred's inline question about the client hostname was answered and the thread is resolved.
Extended reasoning...

Overview

Single-line change in test/js/node/tls/node-tls-connect-hostname-verification.test.ts: the Bun.listen bind hostname moves from "localhost" to "127.0.0.1", plus a comment explaining why. The client-side Bun.connect({hostname: "localhost"}) is unchanged.

Security risks

None. Test-only change to a local listener bind address. No production code, no credentials, no TLS configuration semantics touched.

Level of scrutiny

Low. This is a host-environment de-flake with a well-understood root cause (AI_ADDRCONFIG on the connect side vs. AI_PASSIVE-only on the listen side, causing the listener to land on ::1 while the client only tries 127.0.0.1 on hosts with loopback-only IPv6). The PR description demonstrates Node behaves identically and cites prior art (#35160). The key thing to check is that the test still asserts what it was written to assert — it does, because certificate hostname verification is driven by the client's connect hostname ("localhost"), not the server's bind address.

Other factors

  • The altnameMismatchReason helper ~80 lines up in the same file already uses hostname: "127.0.0.1" for its Bun.listen, so this change brings the two Bun.listen sites into consistency rather than diverging.
  • The tls.connect tests in the first describe block use withServer, which calls server.listen(0) with no host and therefore binds all interfaces — so they don't share this failure mode and correctly weren't touched.
  • Jarred asked whether the client hostname on line 218 should also change; the author explained it must stay "localhost" to keep exercising the DNS-name/CN mismatch path (vs. the IP-SAN path), and the thread is marked resolved.
  • The added comment is durable (documents a non-obvious host-config interaction) and matches repo comment guidance.

@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:05 AM PT - Jul 31st, 2026

@robobun, your commit f581bdc is building: #86204

@Jarred-Sumner
Jarred-Sumner merged commit b78a50d into main Jul 31, 2026
49 of 51 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/2b449d76/tls-hostname-verify-test-bind-ipv4 branch July 31, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants