From a4f92fbe7de44ce2f98f81e2b83b156d6032ecf7 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Fri, 31 Jul 2026 13:45:24 +0000 Subject: [PATCH 1/2] fix(agent): detect IPv6-bound listening ports --- agent/ports_supported.go | 20 ++++++++++++++++-- agent/ports_supported_internal_test.go | 28 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index 30df6caf7ac..d1199b47b21 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -30,13 +30,29 @@ func (lp *osListeningPortsGetter) GetListeningPorts() ([]codersdk.WorkspaceAgent return ports, nil } - tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { + acceptListening := func(s *netstat.SockTabEntry) bool { return s.State == netstat.Listen - }) + } + + tabs, err := netstat.TCPSocks(acceptListening) if err != nil { return nil, xerrors.Errorf("scan listening ports: %w", err) } + // Include IPv6 listeners too. Many dev servers (e.g. Next.js, Node's + // default http.Server) bind to the IPv6 wildcard address "::", which is + // a dual-stack socket that also accepts IPv4 connections, but only shows + // up in the IPv6 socket table (/proc/net/tcp6), not /proc/net/tcp. + // + // The IPv6 scan fails on systems with IPv6 disabled (e.g. missing + // /proc/net/tcp6 on Linux). Fall back to the IPv4 results instead of + // failing the whole scan, otherwise the ports UI breaks entirely on + // such systems. + tabs6, err := netstat.TCP6Socks(acceptListening) + if err == nil { + tabs = append(tabs, tabs6...) + } + seen := make(map[uint16]struct{}, len(tabs)) ports := []codersdk.WorkspaceAgentListeningPort{} for _, tab := range tabs { diff --git a/agent/ports_supported_internal_test.go b/agent/ports_supported_internal_test.go index e16bd8a0c88..f0139b8005a 100644 --- a/agent/ports_supported_internal_test.go +++ b/agent/ports_supported_internal_test.go @@ -43,3 +43,31 @@ func TestOSListeningPortsGetter(t *testing.T) { // note that it's unsafe to try to assert that a port does not exist in the response // because the OS may reallocate the port very quickly. } + +func TestOSListeningPortsGetter_IPv6(t *testing.T) { + t.Parallel() + + uut := &osListeningPortsGetter{ + cacheDuration: 1 * time.Hour, + } + + // Many dev servers (e.g. Next.js) bind to the IPv6 wildcard address, + // which is dual-stack and only shows up in the IPv6 socket table. + l, err := net.Listen("tcp", "[::]:0") + if err != nil { + t.Skipf("unable to listen on IPv6 wildcard address: %s", err) + } + defer l.Close() + + ports, err := uut.GetListeningPorts() + require.NoError(t, err) + found := false + for _, port := range ports { + // #nosec G115 - Safe conversion as TCP port numbers are within uint16 range (0-65535) + if port.Port == uint16(l.Addr().(*net.TCPAddr).Port) { + found = true + break + } + } + require.True(t, found, "port %d not found in %v", l.Addr().(*net.TCPAddr).Port, ports) +} From 474becef83adb85f18d6d1f4e089ca90331f3175 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Mon, 3 Aug 2026 18:36:06 +0000 Subject: [PATCH 2/2] test(agent): hoist TCPAddr cast in IPv6 ports test --- agent/ports_supported_internal_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/agent/ports_supported_internal_test.go b/agent/ports_supported_internal_test.go index f0139b8005a..56f688d601d 100644 --- a/agent/ports_supported_internal_test.go +++ b/agent/ports_supported_internal_test.go @@ -59,15 +59,17 @@ func TestOSListeningPortsGetter_IPv6(t *testing.T) { } defer l.Close() + // #nosec G115 - Safe conversion as TCP port numbers are within uint16 range (0-65535) + want := uint16(l.Addr().(*net.TCPAddr).Port) + ports, err := uut.GetListeningPorts() require.NoError(t, err) found := false for _, port := range ports { - // #nosec G115 - Safe conversion as TCP port numbers are within uint16 range (0-65535) - if port.Port == uint16(l.Addr().(*net.TCPAddr).Port) { + if port.Port == want { found = true break } } - require.True(t, found, "port %d not found in %v", l.Addr().(*net.TCPAddr).Port, ports) + require.True(t, found, "port %d not found in %v", want, ports) }