Repository navigation
Description
Summary
If a process listening on 127.0.0.1:<port> closes that listener and then
listens again from the same PID, the envd port forwarder kills the socat
but never starts a new one. The port then stays unreachable through the
sandbox proxy (The sandbox is running but port is not open, HTTP 502) for as
long as that process lives. This only happens when at least one port scan
runs while the port is closed (gap > ~1s).
This is exactly the root cause described in #3514 ("re-forward a sandbox port
that drops out of a single scan"). That PR was closed without merging on
2026-08-04, and current main (envd 0.10.2,
packages/envd/internal/port/forward.go) still has the behaviour. We couldn't
find an open issue tracking it, so we're opening one to ask whether #3514 was
superseded or can be revived.
Root cause (current main)
In Forwarder.StartForwarding, entries are keyed by "<pid>-<port>". Entries
missing from a scan go through stopPortForwarding, which kills the socat
process group and clears socat/socatPid, but the entry is never removed
from f.ports. When the same PID listens on the port again, the key is found
(portOk == true), the state flips back to FORWARD, and
startPortForwarding is never called.
If the listener comes back with a new PID, a new key is created and a
socat is spawned. So the problem is specific to same-PID re-listens, such as
in-process server restarts, hot reloads, and frameworks that rebind a socket.
Reproduction (official E2B, default code-interpreter template, envd 0.10.0)
Inside a sandbox:
# same_pid_relisten.py
import socket, subprocess, time
PORT = 3000
def listen():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", PORT)); s.listen(); return s
def forwarded():
out = subprocess.run(["sudo", "-n", "ss", "-ltnp"], capture_output=True, text=True).stdout
return any(f"169.254.0.21:{PORT} " in l for l in out.splitlines())
s = listen(); time.sleep(3); print("1. listening, forwarder:", forwarded())
s.close(); time.sleep(3); print("2. closed for 3s, forwarder:", forwarded())
s = listen(); time.sleep(5); print("3. same pid listening again, forwarder:", forwarded())Output:
1. listening, forwarder: True
2. closed for 3s, forwarder: False
3. same pid listening again, forwarder: False <-- expected True
Additional runs (Node HTTP server, same process re-listening):
| Case | Result |
|---|---|
| Same PID, 3s gap, IPv4 only | not re-forwarded (2/2) |
Same PID, 3s gap, dual-stack 127.0.0.1 + ::1 |
not re-forwarded (1/1) |
| Same PID, 100ms gap (no scan in between) | still forwarded |
| New PID on the same port, fast or slow restart | re-forwarded (6/6) |
While the stuck process kept listening, 127.0.0.1:<port> returned 200 inside
the sandbox, but 169.254.0.21:<port> refused connections and the public host
returned 502 The sandbox is running but port is not open.
Real-world impact
We hit this in our development environment, in sandboxes running a Node gateway. Without a
supervisor, that gateway does an in-process restart (same PID) on SIGUSR1.
After one such restart, the gateway's public URL returned 502 permanently,
while the service was healthy on loopback, until the process was replaced.
Expected
A port that is listening on localhost is forwarded again after it reappears,
whatever PID owns it. The fix in #3514 (delete stopped entries from the map,
and run the stop pass before starting new forwards so a fresh socat doesn't
race a dying one for the bind address) looks right to us.
Environment
- Official E2B cloud, default
code-interpretertemplate - envd
0.10.0in the sandbox; same code onmainat envd0.10.2 - Related but different: envd/port-forwarder: IPv6 wildcard (:::PORT) listeners silently ignored; dual-stack port key collision drops one socat #3586 / fix(envd/port): detect :::PORT wildcard listeners and fix dual-stack key collision (rebased #3588) #3717 (dual-stack key collision,
:::PORT
wildcard listeners)
Activity