Skip to content

Commit b1a095e

Browse files
feat: show listening ports in port forward popup (#4389)
* feat: show listening ports in port forward popup * Move fetch logic to a machine * feat: don't show app ports and common non-HTTP ports Co-authored-by: Bruno Quaresma <bruno@coder.com>
1 parent a64731e commit b1a095e

File tree

7 files changed

+456
-94
lines changed

7 files changed

+456
-94
lines changed

coderd/workspaceagents.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net"
1010
"net/http"
1111
"net/netip"
12+
"net/url"
1213
"reflect"
1314
"strconv"
1415
"strings"
@@ -262,6 +263,59 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req
262263
return
263264
}
264265

266+
// Get a list of ports that are in-use by applications.
267+
apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID)
268+
if xerrors.Is(err, sql.ErrNoRows) {
269+
apps = []database.WorkspaceApp{}
270+
err = nil
271+
}
272+
if err != nil {
273+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
274+
Message: "Internal error fetching workspace apps.",
275+
Detail: err.Error(),
276+
})
277+
return
278+
}
279+
appPorts := make(map[uint16]struct{}, len(apps))
280+
for _, app := range apps {
281+
if !app.Url.Valid || app.Url.String == "" {
282+
continue
283+
}
284+
u, err := url.Parse(app.Url.String)
285+
if err != nil {
286+
continue
287+
}
288+
port := u.Port()
289+
if port == "" {
290+
continue
291+
}
292+
portNum, err := strconv.Atoi(port)
293+
if err != nil {
294+
continue
295+
}
296+
if portNum < 1 || portNum > 65535 {
297+
continue
298+
}
299+
appPorts[uint16(portNum)] = struct{}{}
300+
}
301+
302+
// Filter out ports that are globally blocked, in-use by applications, or
303+
// common non-HTTP ports such as databases, FTP, SSH, etc.
304+
filteredPorts := make([]codersdk.ListeningPort, 0, len(portsResponse.Ports))
305+
for _, port := range portsResponse.Ports {
306+
if port.Port < uint16(codersdk.MinimumListeningPort) {
307+
continue
308+
}
309+
if _, ok := appPorts[port.Port]; ok {
310+
continue
311+
}
312+
if _, ok := codersdk.IgnoredListeningPorts[port.Port]; ok {
313+
continue
314+
}
315+
filteredPorts = append(filteredPorts, port)
316+
}
317+
318+
portsResponse.Ports = filteredPorts
265319
httpapi.Write(ctx, rw, http.StatusOK, portsResponse)
266320
}
267321

coderd/workspaceagents_test.go

Lines changed: 200 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7+
"fmt"
78
"net"
89
"runtime"
910
"strconv"
@@ -367,50 +368,124 @@ func TestWorkspaceAgentPTY(t *testing.T) {
367368

368369
func TestWorkspaceAgentListeningPorts(t *testing.T) {
369370
t.Parallel()
370-
client := coderdtest.New(t, &coderdtest.Options{
371-
IncludeProvisionerDaemon: true,
372-
})
373-
coderdPort, err := strconv.Atoi(client.URL.Port())
374-
require.NoError(t, err)
375371

376-
user := coderdtest.CreateFirstUser(t, client)
377-
authToken := uuid.NewString()
378-
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
379-
Parse: echo.ParseComplete,
380-
ProvisionDryRun: echo.ProvisionComplete,
381-
Provision: []*proto.Provision_Response{{
382-
Type: &proto.Provision_Response_Complete{
383-
Complete: &proto.Provision_Complete{
384-
Resources: []*proto.Resource{{
385-
Name: "example",
386-
Type: "aws_instance",
387-
Agents: []*proto.Agent{{
388-
Id: uuid.NewString(),
389-
Auth: &proto.Agent_Token{
390-
Token: authToken,
391-
},
372+
setup := func(t *testing.T, apps []*proto.App) (*codersdk.Client, uint16, uuid.UUID) {
373+
client := coderdtest.New(t, &coderdtest.Options{
374+
IncludeProvisionerDaemon: true,
375+
})
376+
coderdPort, err := strconv.Atoi(client.URL.Port())
377+
require.NoError(t, err)
378+
379+
user := coderdtest.CreateFirstUser(t, client)
380+
authToken := uuid.NewString()
381+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
382+
Parse: echo.ParseComplete,
383+
ProvisionDryRun: echo.ProvisionComplete,
384+
Provision: []*proto.Provision_Response{{
385+
Type: &proto.Provision_Response_Complete{
386+
Complete: &proto.Provision_Complete{
387+
Resources: []*proto.Resource{{
388+
Name: "example",
389+
Type: "aws_instance",
390+
Agents: []*proto.Agent{{
391+
Id: uuid.NewString(),
392+
Auth: &proto.Agent_Token{
393+
Token: authToken,
394+
},
395+
Apps: apps,
396+
}},
392397
}},
393-
}},
398+
},
394399
},
395-
},
396-
}},
397-
})
398-
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
399-
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
400-
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
401-
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
400+
}},
401+
})
402+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
403+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
404+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
405+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
402406

403-
agentClient := codersdk.New(client.URL)
404-
agentClient.SessionToken = authToken
405-
agentCloser := agent.New(agent.Options{
406-
FetchMetadata: agentClient.WorkspaceAgentMetadata,
407-
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
408-
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
409-
})
410-
t.Cleanup(func() {
411-
_ = agentCloser.Close()
412-
})
413-
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
407+
agentClient := codersdk.New(client.URL)
408+
agentClient.SessionToken = authToken
409+
agentCloser := agent.New(agent.Options{
410+
FetchMetadata: agentClient.WorkspaceAgentMetadata,
411+
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
412+
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
413+
})
414+
t.Cleanup(func() {
415+
_ = agentCloser.Close()
416+
})
417+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
418+
419+
return client, uint16(coderdPort), resources[0].Agents[0].ID
420+
}
421+
422+
willFilterPort := func(port int) bool {
423+
if port < codersdk.MinimumListeningPort || port > 65535 {
424+
return true
425+
}
426+
if _, ok := codersdk.IgnoredListeningPorts[uint16(port)]; ok {
427+
return true
428+
}
429+
430+
return false
431+
}
432+
433+
generateUnfilteredPort := func(t *testing.T) (net.Listener, uint16) {
434+
var (
435+
l net.Listener
436+
port uint16
437+
)
438+
require.Eventually(t, func() bool {
439+
var err error
440+
l, err = net.Listen("tcp", "localhost:0")
441+
if err != nil {
442+
return false
443+
}
444+
tcpAddr, _ := l.Addr().(*net.TCPAddr)
445+
if willFilterPort(tcpAddr.Port) {
446+
_ = l.Close()
447+
return false
448+
}
449+
t.Cleanup(func() {
450+
_ = l.Close()
451+
})
452+
453+
port = uint16(tcpAddr.Port)
454+
return true
455+
}, testutil.WaitShort, testutil.IntervalFast)
456+
457+
return l, port
458+
}
459+
460+
generateFilteredPort := func(t *testing.T) (net.Listener, uint16) {
461+
var (
462+
l net.Listener
463+
port uint16
464+
)
465+
require.Eventually(t, func() bool {
466+
for ignoredPort := range codersdk.IgnoredListeningPorts {
467+
if ignoredPort < 1024 || ignoredPort == 5432 {
468+
continue
469+
}
470+
471+
var err error
472+
l, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", ignoredPort))
473+
if err != nil {
474+
continue
475+
}
476+
t.Cleanup(func() {
477+
_ = l.Close()
478+
})
479+
480+
port = ignoredPort
481+
return true
482+
}
483+
484+
return false
485+
}, testutil.WaitShort, testutil.IntervalFast)
486+
487+
return l, port
488+
}
414489

415490
t.Run("LinuxAndWindows", func(t *testing.T) {
416491
t.Parallel()
@@ -419,55 +494,98 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
419494
return
420495
}
421496

422-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
423-
defer cancel()
497+
t.Run("OK", func(t *testing.T) {
498+
t.Parallel()
424499

425-
// Create a TCP listener on a random port that we expect to see in the
426-
// response.
427-
l, err := net.Listen("tcp", "localhost:0")
428-
require.NoError(t, err)
429-
defer l.Close()
430-
tcpAddr, _ := l.Addr().(*net.TCPAddr)
500+
client, coderdPort, agentID := setup(t, nil)
431501

432-
// List ports and ensure that the port we expect to see is there.
433-
res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID)
434-
require.NoError(t, err)
502+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
503+
defer cancel()
435504

436-
var (
437-
expected = map[uint16]bool{
438-
// expect the listener we made
439-
uint16(tcpAddr.Port): false,
440-
// expect the coderdtest server
441-
uint16(coderdPort): false,
442-
}
443-
)
444-
for _, port := range res.Ports {
445-
if port.Network == codersdk.ListeningPortNetworkTCP {
446-
if val, ok := expected[port.Port]; ok {
447-
if val {
448-
t.Fatalf("expected to find TCP port %d only once in response", port.Port)
505+
// Generate a random unfiltered port.
506+
l, lPort := generateUnfilteredPort(t)
507+
508+
// List ports and ensure that the port we expect to see is there.
509+
res, err := client.WorkspaceAgentListeningPorts(ctx, agentID)
510+
require.NoError(t, err)
511+
512+
var (
513+
expected = map[uint16]bool{
514+
// expect the listener we made
515+
lPort: false,
516+
// expect the coderdtest server
517+
coderdPort: false,
518+
}
519+
)
520+
for _, port := range res.Ports {
521+
if port.Network == codersdk.ListeningPortNetworkTCP {
522+
if val, ok := expected[port.Port]; ok {
523+
if val {
524+
t.Fatalf("expected to find TCP port %d only once in response", port.Port)
525+
}
449526
}
527+
expected[port.Port] = true
450528
}
451-
expected[port.Port] = true
452529
}
453-
}
454-
for port, found := range expected {
455-
if !found {
456-
t.Fatalf("expected to find TCP port %d in response", port)
530+
for port, found := range expected {
531+
if !found {
532+
t.Fatalf("expected to find TCP port %d in response", port)
533+
}
457534
}
458-
}
459535

460-
// Close the listener and check that the port is no longer in the response.
461-
require.NoError(t, l.Close())
462-
time.Sleep(2 * time.Second) // avoid cache
463-
res, err = client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID)
464-
require.NoError(t, err)
536+
// Close the listener and check that the port is no longer in the response.
537+
require.NoError(t, l.Close())
538+
time.Sleep(2 * time.Second) // avoid cache
539+
res, err = client.WorkspaceAgentListeningPorts(ctx, agentID)
540+
require.NoError(t, err)
465541

466-
for _, port := range res.Ports {
467-
if port.Network == codersdk.ListeningPortNetworkTCP && port.Port == uint16(tcpAddr.Port) {
468-
t.Fatalf("expected to not find TCP port %d in response", tcpAddr.Port)
542+
for _, port := range res.Ports {
543+
if port.Network == codersdk.ListeningPortNetworkTCP && port.Port == lPort {
544+
t.Fatalf("expected to not find TCP port %d in response", lPort)
545+
}
469546
}
470-
}
547+
})
548+
549+
t.Run("Filter", func(t *testing.T) {
550+
t.Parallel()
551+
552+
// Generate an unfiltered port that we will create an app for and
553+
// should not exist in the response.
554+
_, appLPort := generateUnfilteredPort(t)
555+
app := &proto.App{
556+
Name: "test-app",
557+
Url: fmt.Sprintf("http://localhost:%d", appLPort),
558+
}
559+
560+
// Generate a filtered port that should not exist in the response.
561+
_, filteredLPort := generateFilteredPort(t)
562+
563+
client, coderdPort, agentID := setup(t, []*proto.App{app})
564+
565+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
566+
defer cancel()
567+
568+
res, err := client.WorkspaceAgentListeningPorts(ctx, agentID)
569+
require.NoError(t, err)
570+
571+
sawCoderdPort := false
572+
for _, port := range res.Ports {
573+
if port.Network == codersdk.ListeningPortNetworkTCP {
574+
if port.Port == appLPort {
575+
t.Fatalf("expected to not find TCP port (app port) %d in response", appLPort)
576+
}
577+
if port.Port == filteredLPort {
578+
t.Fatalf("expected to not find TCP port (filtered port) %d in response", filteredLPort)
579+
}
580+
if port.Port == coderdPort {
581+
sawCoderdPort = true
582+
}
583+
}
584+
}
585+
if !sawCoderdPort {
586+
t.Fatalf("expected to find TCP port (coderd port) %d in response", coderdPort)
587+
}
588+
})
471589
})
472590

473591
t.Run("Darwin", func(t *testing.T) {
@@ -477,6 +595,8 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
477595
return
478596
}
479597

598+
client, _, agentID := setup(t, nil)
599+
480600
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
481601
defer cancel()
482602

@@ -486,7 +606,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) {
486606
defer l.Close()
487607

488608
// List ports and ensure that the list is empty because we're on darwin.
489-
res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID)
609+
res, err := client.WorkspaceAgentListeningPorts(ctx, agentID)
490610
require.NoError(t, err)
491611
require.Len(t, res.Ports, 0)
492612
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy