Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8a4adee

Browse files
authored
fix(agent): detect IPv6-bound listening ports (#27765)
Closes #15675 Dev servers that bind to the IPv6 wildcard address (Next.js, Node's default `http.Server`, Go's `net.Listen` on wildcard addresses, and others) never showed up in the dashboard Ports panel, even though they were listening and reachable. The agent's port scanner only called `netstat.TCPSocks`, which reads `/proc/net/tcp` (IPv4 only); an `[::]` dual-stack socket lives exclusively in `/proc/net/tcp6`, so the panel reported "No open ports were detected." Also scan `netstat.TCP6Socks` and merge the results into the existing dedupe loop. The IPv6 scan failure is non-fatal so hosts with IPv6 disabled (missing `/proc/net/tcp6`, Windows with the v6 stack off) keep their IPv4 results instead of the Ports panel 500ing. Both captures below: `python3 -m http.server 5123 --bind ::` running in the same workspace, built from main vs this branch ([full recordings](https://github.com/coder/coder/tree/recordings/recordings/ipv6-listening-ports)). ## Before ![before](https://raw.githubusercontent.com/coder/coder/recordings/recordings/ipv6-listening-ports/before.jpg) ## After ![after](https://raw.githubusercontent.com/coder/coder/recordings/recordings/ipv6-listening-ports/after.jpg) <details> <summary>Debugging and decision log</summary> - Reproduced on a dogfood workspace: `next-server` listening on `*:3000` per `ss -tlnp`, `curl` returning 200, but `GET /api/v2/workspaceagents/{agent}/listening-ports` returning `{"ports": []}`. Port 3000 (hex `0BB8`) present in `/proc/net/tcp6`, absent from `/proc/net/tcp`. - Confirmed `agent/ports_supported.go` only calls `netstat.TCPSocks`; the library's `TCP6Socks` (parses `/proc/net/tcp6` on Linux, `GetTcp6Table2` on Windows) was never referenced. - Control test: an IPv4-bound (`0.0.0.0`) listener was detected correctly, isolating the bug to the missing IPv6 scan. - Made the IPv6 scan failure non-fatal after review: returning an error would break the entire ports endpoint (HTTP 500 on every poll) on IPv6-disabled hosts, a regression from the current behavior of "IPv4 ports only." Silent fallback matches the spirit of `ports_unsupported.go`, which returns an empty list rather than erroring. - The fix lives in the OS-backed `osListeningPortsGetter` behind the `ListeningPortsGetter` interface from #20842, so the fake used by coderd tests is unaffected. - Dedupe by port in the existing `seen` map covers dual-stack sockets that appear in both tables on Windows. - `TestOSListeningPortsGetter_IPv6` listens on `[::]:0` and asserts detection; it skips when the host can't bind IPv6. Verified it fails against the pre-fix code. </details> > 🤖 This PR was opened by [Coder Agents](https://coder.com/docs/ai-coder/agents) on behalf of @bpmct.
1 parent 71c7e92 commit 8a4adee

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

agent/ports_supported.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,29 @@ func (lp *osListeningPortsGetter) GetListeningPorts() ([]codersdk.WorkspaceAgent
3030
return ports, nil
3131
}
3232

33-
tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool {
33+
acceptListening := func(s *netstat.SockTabEntry) bool {
3434
return s.State == netstat.Listen
35-
})
35+
}
36+
37+
tabs, err := netstat.TCPSocks(acceptListening)
3638
if err != nil {
3739
return nil, xerrors.Errorf("scan listening ports: %w", err)
3840
}
3941

42+
// Include IPv6 listeners too. Many dev servers (e.g. Next.js, Node's
43+
// default http.Server) bind to the IPv6 wildcard address "::", which is
44+
// a dual-stack socket that also accepts IPv4 connections, but only shows
45+
// up in the IPv6 socket table (/proc/net/tcp6), not /proc/net/tcp.
46+
//
47+
// The IPv6 scan fails on systems with IPv6 disabled (e.g. missing
48+
// /proc/net/tcp6 on Linux). Fall back to the IPv4 results instead of
49+
// failing the whole scan, otherwise the ports UI breaks entirely on
50+
// such systems.
51+
tabs6, err := netstat.TCP6Socks(acceptListening)
52+
if err == nil {
53+
tabs = append(tabs, tabs6...)
54+
}
55+
4056
seen := make(map[uint16]struct{}, len(tabs))
4157
ports := []codersdk.WorkspaceAgentListeningPort{}
4258
for _, tab := range tabs {

agent/ports_supported_internal_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,33 @@ func TestOSListeningPortsGetter(t *testing.T) {
4343
// note that it's unsafe to try to assert that a port does not exist in the response
4444
// because the OS may reallocate the port very quickly.
4545
}
46+
47+
func TestOSListeningPortsGetter_IPv6(t *testing.T) {
48+
t.Parallel()
49+
50+
uut := &osListeningPortsGetter{
51+
cacheDuration: 1 * time.Hour,
52+
}
53+
54+
// Many dev servers (e.g. Next.js) bind to the IPv6 wildcard address,
55+
// which is dual-stack and only shows up in the IPv6 socket table.
56+
l, err := net.Listen("tcp", "[::]:0")
57+
if err != nil {
58+
t.Skipf("unable to listen on IPv6 wildcard address: %s", err)
59+
}
60+
defer l.Close()
61+
62+
// #nosec G115 - Safe conversion as TCP port numbers are within uint16 range (0-65535)
63+
want := uint16(l.Addr().(*net.TCPAddr).Port)
64+
65+
ports, err := uut.GetListeningPorts()
66+
require.NoError(t, err)
67+
found := false
68+
for _, port := range ports {
69+
if port.Port == want {
70+
found = true
71+
break
72+
}
73+
}
74+
require.True(t, found, "port %d not found in %v", want, ports)
75+
}

0 commit comments

Comments
 (0)