Skip to content

Fix crash when a container connects to an exposed host port with nothing listening - #1617

Merged
cristianrgreco merged 1 commit into
mainfrom
claude/practical-dewdney-650886
Oct 25, 2026
Merged

cristianrgreco merged 1 commit into
mainfrom
claude/practical-dewdney-650886

Conversation

@cristianrgreco

Copy link
Copy Markdown
Collaborator

Summary

TestContainers.exposeHostPorts crashed the test process with an uncaught ECONNREFUSED when a container connected to host.testcontainers.internal:<port> and nothing was listening on that host port. The container side of the connection also hung until its own timeout.

The cause was in ssh-remote-port-forward@1.0.4. For each forwarded connection it opens a host socket with net.connect but attaches no error listener, and it never closes the SSH channel when that socket fails.

That library is tiny and hasn't been touched since 2021, so this PR drops it and does the forwarding in port-forwarder.ts with ssh2 directly. ssh2 was already in the tree through docker-modem. The new code keeps the old behaviour:

  • connect resolves on ready
  • forwardIn is used to expose a port
  • the SSH socket is unref'd once connected
  • readyTimeout: 100_000 is still passed when reusing a port forwarder

It also adds two error handlers:

  • When the host socket fails, the SSH channel is closed, so the container sees the connection close (curl: empty reply) instead of hanging.
  • When the SSH channel fails, the host socket is destroyed.

Two smaller changes come with it:

  • The SSH socket is now created by us and passed to ssh2 as sock. This lets us unref() it without reaching into ssh2's private _sock, which the old library did.
  • Errors before ready (for example, failing to reach the sshd container) now reject exposeHostPorts. They used to be emitted on a client with no error listener, which also crashed the process.

Dependencies:

  • ssh-remote-port-forward is replaced by ssh2@^1.15.0 (the lockfile keeps the 1.15.0 already installed).
  • @types/ssh2@^1.15.6 is added as a devDependency. It replaces the 0.5.x typings that the old library pulled in; those described the pre-1.0 ssh2 API.

Verification

Regression test added to port-forwarder.test.ts: it exposes a host port with no server behind it, then curls it from a container and expects exit code 52 (empty reply).

Red, on the unfixed code:

× should close the connection when nothing listens on the exposed host port 13194ms
AssertionError: expected 28 to be 52 // Object.is equality
Unhandled Errors: Uncaught Exception AggregateError ECONNREFUSED (connect ECONNREFUSED ::1:60463, connect ECONNREFUSED 127.0.0.1:60463)
Tests  1 failed | 3 passed (4)
Errors  1 error

Green, with the fix (port-forwarder.test.ts and port-forwarder-reuse.test.ts):

✓ port-forwarder-reuse.test.ts > should expose additional ports 330ms
✓ port-forwarder-reuse.test.ts > should reuse same ports 271ms
✓ port-forwarder.test.ts > should expose host ports to the container 814ms
✓ port-forwarder.test.ts > should expose host ports to the container with custom network 964ms
✓ port-forwarder.test.ts > should close the connection when nothing listens on the exposed host port 1350ms
✓ port-forwarder.test.ts > should expose host ports to the container with custom network and network alias 1548ms
Tests  6 passed (6)

A plain Node script that calls exposeHostPorts against the built package still exits as soon as it is done, so the unref'd SSH socket doesn't keep the process alive.

npm run format, npm run lint, npm run knip and npm run check-compiles all pass.

Not breaking

No public API changes. The new runtime dependency ssh2 is CommonJS and was already installed through dockerode → docker-modem. @types/ssh2 is a devDependency, and the port forwarder's types aren't reachable from index.d.ts.

Closes #1607

…ing listening

Forward SSH remote connections with ssh2 directly instead of
ssh-remote-port-forward, closing the SSH channel when the host
connection fails rather than leaving the socket error unhandled.

Closes #1607
@cristianrgreco cristianrgreco added bug Something isn't working patch Backward compatible bug fix labels Oct 25, 2026
@cristianrgreco
cristianrgreco marked this pull request as ready for review October 25, 2026 09:50
@cristianrgreco
cristianrgreco merged commit c718c35 into main Oct 25, 2026
191 checks passed
@cristianrgreco
cristianrgreco deleted the claude/practical-dewdney-650886 branch October 25, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working patch Backward compatible bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exposeHostPorts: connection to a closed host port causes an uncaught ECONNREFUSED in the test process

1 participant