Skip to content

fix: populate localAddress on the connect handler's Socket#6647

Closed
xortive wants to merge 1 commit intocloudflare:mainfrom
xortive:fix/connect-handler-remote-address
Closed

fix: populate localAddress on the connect handler's Socket#6647
xortive wants to merge 1 commit intocloudflare:mainfrom
xortive:fix/connect-handler-remote-address

Conversation

@xortive
Copy link
Copy Markdown
Contributor

@xortive xortive commented Apr 23, 2026

Summary

  • When a worker exports a connect() handler, socket.opened.localAddress (and previously remoteAddress) currently always resolves to undefined because ServiceWorkerGlobalScope::connect discards the CONNECT authority when constructing the ingress Socket.
  • Plumb a new localAddress parameter through Socket / setupSocket and forward the host parameter into it. JS callers now observe the exact authority string that was passed to fetcher.connect("host:port"), on the correct field.
  • Extend the existing connect-handler-test with a localAddressViaServiceBinding case that asserts strict equality between the authority passed to fetcher.connect(...) and the localAddress observed on the server side.

Semantics: why localAddress, not remoteAddress

On the ingress (handler) side of a CONNECT tunnel, the authority the caller targeted (e.g. "example.com:1234") represents the address on this side of the socket — the endpoint the peer asked to connect to. That's the local address from the handler's perspective, not the remote address. The outbound connect() side continues to populate remoteAddress as before; localAddress remains empty on outbound since there is no useful value to expose.

SocketInfo.localAddress previously carried a comment saying it "will always remain empty" — updated to reflect the new behavior.

Changes

  • src/workerd/api/sockets.h:
    • Added localAddress to the Socket constructor parameter list and as a member.
    • Added localAddress parameter to the setupSocket free-function declaration.
    • Updated the SocketInfo.localAddress comment.
  • src/workerd/api/sockets.c++:
    • Threaded localAddress through setupSocket, Socket::startTls, and both Socket::handleProxyStatus overloads.
  • src/workerd/api/global-scope.c++:
    • Forward kj::mv(host) into the new localAddress argument of setupSocket.
  • src/workerd/api/hyperdrive.c++:
    • Pass kj::none /* localAddress */ (outbound, unchanged behavior).
  • Tests:
    • Added a TARGET service binding to the existing connect-handler-test worker pointing at a new LocalAddressEndpoint defined alongside the existing ConnectProxy / ConnectEndpoint in connect-handler-test-proxy.js.
    • Added a localAddressViaServiceBinding case to connect-handler-test.js that calls env.TARGET.connect("example.com:1234") and asserts the server reports back exactly that string as info.localAddress.

The TCP listener path in workerd's TcpListener forwards the listener's bound address (e.g. *:8081) as the CONNECT authority rather than the client's connect() string, so the existing TCP-listener tests cannot give a strict-equality guarantee against the caller-supplied authority. The service-binding path goes through WorkerEntrypoint::connect and forwards the authority verbatim, which is what end-to-end exercises this fix.

Verification

Red-green

  • Red: With the global-scope.c++ forwarding reverted, localAddressViaServiceBinding fails with Actual: "OK:undefined" — confirming the test catches the bug. The other two existing test cases (connectHandler, connectHandlerProxy) keep passing since they don't depend on localAddress.
  • Green: With the fix, all three test cases pass.

Broader suite

bazel test //src/workerd/api/... --nocache_test_results: 906/912 pass. The 6 failures (worker-loader-test:pythonBasics and dns-nodejs-test) are pre-existing on clean main (verified by stashing the change and rerunning before any code modifications) — they fail due to environmental TLS cert chain / DNS issues on the developer machine and are unrelated to this change.

Non-goals

  • Not populating the domain / SNI parameter — incoming CONNECT with TLS is still rejected upstream in worker-entrypoint.c++.
  • No change to the shape of SocketInfo's public interface: both remoteAddress and localAddress fields already exist on the JS surface; localAddress was simply never populated before.
  • Outbound connect() behavior is preserved: remoteAddress continues to carry the target address; localAddress remains empty on outbound.

@xortive xortive requested review from a team as code owners April 23, 2026 17:26
const payload =
info.remoteAddress === EXPECTED_AUTHORITY
? 'OK'
: `BAD:${info.remoteAddress}`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I honestly think this should be localAddress here.
That fits socket semantics more imo.

@kentonv wdyt?

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.

Yeah, on the server side, "remoteAddress" should be the client's address, not the server's address.

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.

Remember the socket object here is not the same socket object that the client holds. It's a socket representing the other end of the connection. So local vs. remote are swapped.

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.

Switched to localAddress. Threaded a localAddress parameter through Socket / setupSocket and forward host into it on the ingress path; outbound continues to populate remoteAddress as before.

// info.remoteAddress matches the authority string the caller supplied to
// fetcher.connect(...). Writes 'OK' on match, 'BAD:<actual>' on mismatch so
// the client's assertion failure points directly at the observed value.
export class RemoteAddressEndpoint extends WorkerEntrypoint {
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.

Can we update the existing tests at src/workerd/api/tests/connect-handler-test.js for this instead of adding a new test that duplicates a lot of the code from it? I think that should be cleaner and easier to maintain in the long 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.

Folded into the existing test. Added a localAddressViaServiceBinding case in connect-handler-test.js that goes through a service binding (TARGET) to a new LocalAddressEndpoint (added to connect-handler-test-proxy.js), with strict equality between the authority passed to fetcher.connect(...) and the localAddress observed on the server side. The TCP listener path can't give that strict-equality guarantee since workerd's TcpListener forwards the listener's bound address (e.g. *:8081) as the CONNECT authority rather than the client's connect() string, so the service-binding path is what actually exercises the fix end-to-end. Removed the separate connect-handler-local-address-test files.

@xortive xortive force-pushed the fix/connect-handler-remote-address branch from ca473a0 to a182141 Compare April 24, 2026 19:33
@xortive xortive changed the title fix: populate remoteAddress on the connect handler's Socket fix: populate localAddress on the connect handler's Socket Apr 24, 2026
@danlapid
Copy link
Copy Markdown
Collaborator

LGTM

@xortive xortive force-pushed the fix/connect-handler-remote-address branch from a182141 to 4a3822b Compare April 27, 2026 18:00
@xortive
Copy link
Copy Markdown
Contributor Author

xortive commented Apr 27, 2026

last push was to cleanup the tests as @fhanau suggested

Copy link
Copy Markdown
Contributor

@fhanau fhanau left a comment

Choose a reason for hiding this comment

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

Thank you for updating the test, LGTM

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 27, 2026

Merging this PR will improve performance by 38.55%

⚡ 1 improved benchmark
✅ 71 untouched benchmarks
⏩ 129 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
simpleStringBody[Response] 27.1 µs 19.6 µs +38.55%

Comparing xortive:fix/connect-handler-remote-address (4a3822b) with main (39b2fc7)

Open in CodSpeed

Footnotes

  1. 129 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

ServiceWorkerGlobalScope::connect was dropping the CONNECT authority when
constructing the ingress Socket, causing both socket.opened.remoteAddress
and socket.opened.localAddress to resolve to undefined on the connect
handler side. Plumb a localAddress parameter through Socket/setupSocket
and forward the host parameter into it so JS callers see the exact
authority string that was passed to fetcher.connect(...).

From the handler's perspective the CONNECT authority is the local address
on this side of the tunnel (the address the peer asked to connect to),
not the remote address, so populate localAddress rather than
remoteAddress. Outbound sockets continue to populate remoteAddress as
before; localAddress remains empty for outbound since we have no useful
value to expose.

Extend the existing connect-handler-test with a localAddressViaServiceBinding
case that asserts strict equality between the authority passed to
fetcher.connect(...) and the localAddress observed on the server side.
@fhanau
Copy link
Copy Markdown
Contributor

fhanau commented May 1, 2026

Merged in #6688.

@fhanau fhanau closed this May 1, 2026
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.

5 participants