e2b-dev / e2b-dev/runtime

envd/port-forwarder: IPv6 wildcard (:::PORT) listeners silently ignored; dual-stack port key collision drops one socat

Open
#3,586 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
1.6k
Forks
438
PR merge metrics
No merged PRs in 30d

Description

Symptom

Case A — a user starts a gRPC server (or any modern framework that defaults to IPv6 wildcard binding) inside a sandbox. The service is running, but the port is never accessible from outside:

# inside sandbox
python -m grpc_tools ... &
ss -tlnp | grep 50051
# LISTEN  0  4096  *:50051  *:*   ← kernel reports dual-stack :::50051

# from outside — never connects
grpcurl -plaintext <sandbox-host>:50051 list
# timeout / connection refused

Case B — a service listening on both 127.0.0.1:PORT and ::1:PORT gets only one socat. Which address socat connects to depends on the nondeterministic order that /proc/net/tcp and /proc/net/tcp6 entries are returned. If socat picks the wrong family the port becomes inaccessible with no error.

Root cause

Bug A — :::PORT wildcard not in the scan filter

packages/envd/internal/port/forward.go:79:

&ScannerFilter{
    IPs:   []string{"127.0.0.1", "localhost", "::1"},
    State: "LISTEN",
},

packages/envd/internal/port/scanfilter.go:20:

ipMatch := slices.Contains(sf.IPs, proc.Laddr.IP)

net.Connections("tcp") reads both /proc/net/tcp and /proc/net/tcp6 (confirmed by the comment in scan.go:42). For an IPv6 wildcard socket, the kernel reports Laddr.IP = "::". "::" is not in the filter list → the port is never detected → no socat is started → service is inaccessible.

With IPv6 enabled in the guest (ipv6.disable=0, see #3585), frameworks that auto-select the bind address on a dual-stack kernel produce :::PORT rather than 0.0.0.0:PORT:

Framework / runtime Default bind Laddr.IP reported Detected?
gRPC-Go, gRPC-Python :::PORT "::"
Node.js net.createServer().listen(PORT) on dual-stack :::PORT "::"
Python socket.AF_INET6 with IPV6_V6ONLY=0 :::PORT "::"
Python uvicorn --host 0.0.0.0 0.0.0.0:PORT (IPv4 only) ❌ (expected)
127.0.0.1:PORT 127.0.0.1:PORT "127.0.0.1"
::1:PORT ::1:PORT "::1"
Bug B — port key collision drops one socat for dual-stack services

packages/envd/internal/port/forward.go:127:

key := fmt.Sprintf("%d-%d", p.Pid, p.Laddr.Port)

When a service listens on both 127.0.0.1:PORT (AF_INET) and ::1:PORT (AF_INET6), both ConnectionStat entries share the same PID and port number. Both pass the filter. The scan loop processes them sequentially:

Iteration 1: key="PID-PORT" not in map → create PortToForward{family=4}, start socat TCP4:localhost:PORT
Iteration 2: key="PID-PORT" already in map → just mark state=FORWARD, skip startPortForwarding

Only one socat is started, for whichever family /proc/net/tcp vs /proc/net/tcp6 returns first. If the app only responds on the skipped family the port silently breaks. The entry order is not guaranteed.

Bug C (secondary) — TCP6:localhost resolution fails on minimal images

packages/envd/internal/port/forward.go:175:

fmt.Sprintf("TCP%d:localhost:%v", p.family, p.port)

For family=6 socat uses TCP6:localhost:PORT. This requires /etc/hosts to contain ::1 localhost. Alpine Linux and many stripped base images ship only 127.0.0.1 localhost, so socat's name resolution returns no AAAA entry → connection fails → ::1 listeners are inaccessible even though the filter correctly detected them.

Why it matters

  • gRPC is the most common affected framework — its default listener is :::PORT on any dual-stack host
  • FastAPI / uvicorn with --host ::, Go net.Listen("tcp", ":PORT") on dual-stack kernels, all produce :::PORT
  • These services start and appear healthy inside the sandbox, but are completely unreachable from outside — no error, just a silent port forwarding gap

Proposed fix

Fix A — add "::" to the filter and normalize wildcard connections to the IPv4 path:

// forward.go:79
IPs: []string{"127.0.0.1", "localhost", "::1", "::"},

// forward.go:148 — wildcard binds accept both families; connect via IPv4 localhost
family: func() uint32 {
    if p.Laddr.IP == "::" {
        return 4
    }
    return familyToIPVersion(p.Family)
}(),

Fix B — include the IP in the port key to avoid collision:

// Before
key := fmt.Sprintf("%d-%d", p.Pid, p.Laddr.Port)

// After
key := fmt.Sprintf("%d-%d-%s", p.Pid, p.Laddr.Port, p.Laddr.IP)

Fix C — use literal addresses instead of hostname resolution:

// Before
fmt.Sprintf("TCP%d:localhost:%v", p.family, p.port)

// After
backendAddr := "127.0.0.1"
if p.family == 6 {
    backendAddr = "[::1]"
}
fmt.Sprintf("TCP%d:%s:%v", p.family, backendAddr, p.port)

Note: fixing #3585 (ipv6.disable=1) eliminates the condition that causes dual-stack :::PORT binding in the first place — on an IPv4-only kernel the same bind("::") call falls back to 0.0.0.0. However Bugs B and C are independently correctness issues that should be fixed regardless of the IPv6 kernel setting.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/envd/internal/port/forward.go and scanfilter.go, then read the connection handling described in scan.go. Reproduce listeners on ::, 127.0.0.1, and ::1 and trace detection, keying, and socat backend selection. Done means IPv6 wildcard services are detected, dual-stack entries are not collapsed, and IPv6 loopback forwarding works without hostname resolution.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, linux
Domain
infrastructure, networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.