From 9abf8bc828a62535895ca28d741d6945aba5ec84 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 13:16:11 +0000 Subject: [PATCH 1/5] feat: show listening ports in port forward popup --- agent/ports_supported.go | 3 +- agent/ports_unsupported.go | 3 +- site/src/api/api.ts | 7 +++ .../PortForwardButton/PortForwardButton.tsx | 63 ++++++++++++++++--- site/src/components/Resources/Resources.tsx | 1 + 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index e405aa6c1bbc1..0455717b0b4b8 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -1,5 +1,4 @@ -//go:build linux || windows -// +build linux windows +//go:build linux || (windows && amd64) package agent diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go index 2eabdaca330ac..005c2f853e75a 100644 --- a/agent/ports_unsupported.go +++ b/agent/ports_unsupported.go @@ -1,5 +1,4 @@ -//go:build !linux && !windows -// +build !linux,!windows +//go:build !linux && !(windows && amd64) package agent diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2ee6fac685c00..90571e0535483 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -497,3 +497,10 @@ export const getWorkspaceQuota = async (userID: string): Promise => { + const response = await axios.get(`/api/v2/workspaceagents/${agentID}/listening-ports`) + return response.data +} diff --git a/site/src/components/PortForwardButton/PortForwardButton.tsx b/site/src/components/PortForwardButton/PortForwardButton.tsx index 593f85d4f79ef..c0420762cfaff 100644 --- a/site/src/components/PortForwardButton/PortForwardButton.tsx +++ b/site/src/components/PortForwardButton/PortForwardButton.tsx @@ -6,25 +6,42 @@ import TextField from "@material-ui/core/TextField" import OpenInNewOutlined from "@material-ui/icons/OpenInNewOutlined" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { Stack } from "components/Stack/Stack" -import { useRef, useState } from "react" +import { useRef, useState, useEffect, Fragment } from "react" import { colors } from "theme/colors" import { CodeExample } from "../CodeExample/CodeExample" +import { ListeningPort } from "api/typesGenerated" import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip" +import { getAgentListeningPorts } from "api/api" export interface PortForwardButtonProps { host: string username: string workspaceName: string agentName: string + agentID: string } const EnabledView: React.FC = (props) => { - const { host, workspaceName, agentName, username } = props + const { host, workspaceName, agentName, agentID, username } = props const styles = useStyles() const [port, setPort] = useState("3000") const { location } = window const urlExample = `${location.protocol}//${port}--${agentName}--${workspaceName}--${username}.${host}` + // Load listening ports from the server and display them as a list below the + // text box. + const [ports, setPorts] = useState([]) + useEffect(() => { + getAgentListeningPorts(agentID) + .then((res) => { + setPorts(res.ports.filter((p) => p.network === "tcp")) + }) + .catch(() => { + // Do nothing. It's not the end of the world if the user can't see the + // listening ports. + }) + }, [agentID, setPorts]) + return ( @@ -57,26 +74,58 @@ const EnabledView: React.FC = (props) => { + + 0}> + + {ports.map((p, i) => { + const url = `${location.protocol}//${p.port}--${agentName}--${workspaceName}--${username}.${host}` + let label = `${p.port}` + if (p.process_name) { + label = `${p.process_name} - ${p.port}` + } + + return ( + + {i > 0 && ·} + + {label} + + + ) + })} + + + + + - Learn more about port forward + Learn more about web port forwarding ) } -const DisabledView: React.FC = () => { +const DisabledView: React.FC = ({ workspaceName, agentName }) => { + const cliExample = `coder port-forward ${workspaceName}.${agentName} --tcp 3000` return ( - Your deployment does not have port forward enabled. See the docs for more - details. + Your deployment does not have web port forwarding enabled. See the docs for + more details. + + You can use the Coder CLI to forward ports from your workspace to your local machine, as + shown below. + + + + - Learn more about port forward + Learn more about web port forwarding diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 3ee0ce745a0f5..498408a7838a0 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -158,6 +158,7 @@ export const Resources: FC> = ({ host={applicationsHost} workspaceName={workspace.name} agentName={agent.name} + agentID={agent.id} username={workspace.owner_name} /> )} From 5eb9435762ea7e2de744fed2b9117a5397d65ffc Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 13:25:51 +0000 Subject: [PATCH 2/5] fixup! feat: show listening ports in port forward popup --- site/src/components/PortForwardButton/PortForwardButton.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/PortForwardButton/PortForwardButton.tsx b/site/src/components/PortForwardButton/PortForwardButton.tsx index c0420762cfaff..0ebd1f65cf2a6 100644 --- a/site/src/components/PortForwardButton/PortForwardButton.tsx +++ b/site/src/components/PortForwardButton/PortForwardButton.tsx @@ -34,6 +34,7 @@ const EnabledView: React.FC = (props) => { useEffect(() => { getAgentListeningPorts(agentID) .then((res) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition setPorts(res.ports.filter((p) => p.network === "tcp")) }) .catch(() => { From e4bbbe44fbe0b1ce3903faabe288919f71f3d9ea Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 13:31:49 +0000 Subject: [PATCH 3/5] fixup! feat: show listening ports in port forward popup --- agent/ports_supported.go | 3 ++- agent/ports_unsupported.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index 0455717b0b4b8..e405aa6c1bbc1 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -1,4 +1,5 @@ -//go:build linux || (windows && amd64) +//go:build linux || windows +// +build linux windows package agent diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go index 005c2f853e75a..2eabdaca330ac 100644 --- a/agent/ports_unsupported.go +++ b/agent/ports_unsupported.go @@ -1,4 +1,5 @@ -//go:build !linux && !(windows && amd64) +//go:build !linux && !windows +// +build !linux,!windows package agent From 39c12971504e9ba62d005634fc60814b1202106f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 6 Oct 2022 16:59:16 +0000 Subject: [PATCH 4/5] Move fetch logic to a machine --- .../PortForwardButton/PortForwardButton.tsx | 78 ++++++++++--------- site/src/components/Resources/Resources.tsx | 2 +- .../portForward/portForwardXService.ts | 46 +++++++++++ 3 files changed, 88 insertions(+), 38 deletions(-) create mode 100644 site/src/xServices/portForward/portForwardXService.ts diff --git a/site/src/components/PortForwardButton/PortForwardButton.tsx b/site/src/components/PortForwardButton/PortForwardButton.tsx index 0ebd1f65cf2a6..25c314a7426ed 100644 --- a/site/src/components/PortForwardButton/PortForwardButton.tsx +++ b/site/src/components/PortForwardButton/PortForwardButton.tsx @@ -6,55 +6,50 @@ import TextField from "@material-ui/core/TextField" import OpenInNewOutlined from "@material-ui/icons/OpenInNewOutlined" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { Stack } from "components/Stack/Stack" -import { useRef, useState, useEffect, Fragment } from "react" +import { useRef, useState, Fragment } from "react" import { colors } from "theme/colors" import { CodeExample } from "../CodeExample/CodeExample" -import { ListeningPort } from "api/typesGenerated" -import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip" -import { getAgentListeningPorts } from "api/api" +import { + HelpTooltipLink, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, +} from "../Tooltips/HelpTooltip" +import { Maybe } from "components/Conditionals/Maybe" +import { useMachine } from "@xstate/react" +import { portForwardMachine } from "xServices/portForward/portForwardXService" export interface PortForwardButtonProps { host: string username: string workspaceName: string agentName: string - agentID: string + agentId: string } const EnabledView: React.FC = (props) => { - const { host, workspaceName, agentName, agentID, username } = props + const { host, workspaceName, agentName, agentId, username } = props const styles = useStyles() const [port, setPort] = useState("3000") const { location } = window const urlExample = `${location.protocol}//${port}--${agentName}--${workspaceName}--${username}.${host}` - - // Load listening ports from the server and display them as a list below the - // text box. - const [ports, setPorts] = useState([]) - useEffect(() => { - getAgentListeningPorts(agentID) - .then((res) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - setPorts(res.ports.filter((p) => p.network === "tcp")) - }) - .catch(() => { - // Do nothing. It's not the end of the world if the user can't see the - // listening ports. - }) - }, [agentID, setPorts]) + const [state] = useMachine(portForwardMachine, { + context: { agentId: agentId }, + }) + const ports = state.context.listeningPorts?.ports return ( - + <> Access ports running on the agent with the port, agent name, workspace name{" "} and your username URL schema, as shown below. - + Use the form to open applications in a new tab. - + = (props) => { - - 0}> - - {ports.map((p, i) => { + 0)}> + + {ports && + ports.map((p, i) => { const url = `${location.protocol}//${p.port}--${agentName}--${workspaceName}--${username}.${host}` let label = `${p.port}` if (p.process_name) { @@ -94,24 +89,24 @@ const EnabledView: React.FC = (props) => { ) })} - - - - + + Learn more about web port forwarding - + ) } const DisabledView: React.FC = ({ workspaceName, agentName }) => { const cliExample = `coder port-forward ${workspaceName}.${agentName} --tcp 3000` + const styles = useStyles() + return ( - + <> Your deployment does not have web port forwarding enabled. See the docs for more details. @@ -122,14 +117,14 @@ const DisabledView: React.FC = ({ workspaceName, agentNa shown below. - + Learn more about web port forwarding - + ) } @@ -171,6 +166,7 @@ export const PortForwardButton: React.FC = (props) => { horizontal: "left", }} > + Port forward @@ -187,7 +183,7 @@ export const PortForwardButton: React.FC = (props) => { const useStyles = makeStyles((theme) => ({ popoverPaper: { padding: `${theme.spacing(2.5)}px ${theme.spacing(3.5)}px ${theme.spacing(3.5)}px`, - width: theme.spacing(46), + width: theme.spacing(52), color: theme.palette.text.secondary, marginTop: theme.spacing(0.25), }, @@ -202,4 +198,12 @@ const useStyles = makeStyles((theme) => ({ borderColor: colors.gray[10], }, }, + + code: { + margin: theme.spacing(2, 0), + }, + + form: { + margin: theme.spacing(1.5, 0, 0), + }, })) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 498408a7838a0..926177e3395a9 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -158,7 +158,7 @@ export const Resources: FC> = ({ host={applicationsHost} workspaceName={workspace.name} agentName={agent.name} - agentID={agent.id} + agentId={agent.id} username={workspace.owner_name} /> )} diff --git a/site/src/xServices/portForward/portForwardXService.ts b/site/src/xServices/portForward/portForwardXService.ts new file mode 100644 index 0000000000000..27fa2bfd4cc56 --- /dev/null +++ b/site/src/xServices/portForward/portForwardXService.ts @@ -0,0 +1,46 @@ +import { getAgentListeningPorts } from "api/api" +import { ListeningPortsResponse } from "api/typesGenerated" +import { createMachine, assign } from "xstate" + +export const portForwardMachine = createMachine( + { + id: "portForwardMachine", + schema: { + context: {} as { + agentId: string + listeningPorts?: ListeningPortsResponse + }, + services: {} as { + getListeningPorts: { + data: ListeningPortsResponse + } + }, + }, + tsTypes: {} as import("./portForwardXService.typegen").Typegen0, + initial: "loading", + states: { + loading: { + invoke: { + src: "getListeningPorts", + onDone: { + target: "success", + actions: ["assignListeningPorts"], + }, + }, + }, + success: { + type: "final", + }, + }, + }, + { + services: { + getListeningPorts: ({ agentId }) => getAgentListeningPorts(agentId), + }, + actions: { + assignListeningPorts: assign({ + listeningPorts: (_, { data }) => data, + }), + }, + }, +) From c002aa21db6a0830623208de4e712d8b8b881a7f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 10 Oct 2022 13:30:31 +0000 Subject: [PATCH 5/5] feat: don't show app ports and common non-HTTP ports --- coderd/workspaceagents.go | 54 +++++++ coderd/workspaceagents_test.go | 280 +++++++++++++++++++++++---------- codersdk/agentconn.go | 72 +++++++++ 3 files changed, 326 insertions(+), 80 deletions(-) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index f7301b9bd38eb..feabf5d0a484b 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/netip" + "net/url" "reflect" "strconv" "strings" @@ -262,6 +263,59 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req return } + // Get a list of ports that are in-use by applications. + apps, err := api.Database.GetWorkspaceAppsByAgentID(ctx, workspaceAgent.ID) + if xerrors.Is(err, sql.ErrNoRows) { + apps = []database.WorkspaceApp{} + err = nil + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace apps.", + Detail: err.Error(), + }) + return + } + appPorts := make(map[uint16]struct{}, len(apps)) + for _, app := range apps { + if !app.Url.Valid || app.Url.String == "" { + continue + } + u, err := url.Parse(app.Url.String) + if err != nil { + continue + } + port := u.Port() + if port == "" { + continue + } + portNum, err := strconv.Atoi(port) + if err != nil { + continue + } + if portNum < 1 || portNum > 65535 { + continue + } + appPorts[uint16(portNum)] = struct{}{} + } + + // Filter out ports that are globally blocked, in-use by applications, or + // common non-HTTP ports such as databases, FTP, SSH, etc. + filteredPorts := make([]codersdk.ListeningPort, 0, len(portsResponse.Ports)) + for _, port := range portsResponse.Ports { + if port.Port < uint16(codersdk.MinimumListeningPort) { + continue + } + if _, ok := appPorts[port.Port]; ok { + continue + } + if _, ok := codersdk.IgnoredListeningPorts[port.Port]; ok { + continue + } + filteredPorts = append(filteredPorts, port) + } + + portsResponse.Ports = filteredPorts httpapi.Write(ctx, rw, http.StatusOK, portsResponse) } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index e4bbe42a5af6d..6bd569dde9f71 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "fmt" "net" "runtime" "strconv" @@ -367,50 +368,124 @@ func TestWorkspaceAgentPTY(t *testing.T) { func TestWorkspaceAgentListeningPorts(t *testing.T) { t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }) - coderdPort, err := strconv.Atoi(client.URL.Port()) - require.NoError(t, err) - user := coderdtest.CreateFirstUser(t, client) - authToken := uuid.NewString() - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionDryRun: echo.ProvisionComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agents: []*proto.Agent{{ - Id: uuid.NewString(), - Auth: &proto.Agent_Token{ - Token: authToken, - }, + setup := func(t *testing.T, apps []*proto.App) (*codersdk.Client, uint16, uuid.UUID) { + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + coderdPort, err := strconv.Atoi(client.URL.Port()) + require.NoError(t, err) + + user := coderdtest.CreateFirstUser(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + Apps: apps, + }}, }}, - }}, + }, }, - }, - }}, - }) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) - coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - agentClient := codersdk.New(client.URL) - agentClient.SessionToken = authToken - agentCloser := agent.New(agent.Options{ - FetchMetadata: agentClient.WorkspaceAgentMetadata, - CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet, - Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug), - }) - t.Cleanup(func() { - _ = agentCloser.Close() - }) - resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agent.Options{ + FetchMetadata: agentClient.WorkspaceAgentMetadata, + CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet, + Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + return client, uint16(coderdPort), resources[0].Agents[0].ID + } + + willFilterPort := func(port int) bool { + if port < codersdk.MinimumListeningPort || port > 65535 { + return true + } + if _, ok := codersdk.IgnoredListeningPorts[uint16(port)]; ok { + return true + } + + return false + } + + generateUnfilteredPort := func(t *testing.T) (net.Listener, uint16) { + var ( + l net.Listener + port uint16 + ) + require.Eventually(t, func() bool { + var err error + l, err = net.Listen("tcp", "localhost:0") + if err != nil { + return false + } + tcpAddr, _ := l.Addr().(*net.TCPAddr) + if willFilterPort(tcpAddr.Port) { + _ = l.Close() + return false + } + t.Cleanup(func() { + _ = l.Close() + }) + + port = uint16(tcpAddr.Port) + return true + }, testutil.WaitShort, testutil.IntervalFast) + + return l, port + } + + generateFilteredPort := func(t *testing.T) (net.Listener, uint16) { + var ( + l net.Listener + port uint16 + ) + require.Eventually(t, func() bool { + for ignoredPort := range codersdk.IgnoredListeningPorts { + if ignoredPort < 1024 || ignoredPort == 5432 { + continue + } + + var err error + l, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", ignoredPort)) + if err != nil { + continue + } + t.Cleanup(func() { + _ = l.Close() + }) + + port = ignoredPort + return true + } + + return false + }, testutil.WaitShort, testutil.IntervalFast) + + return l, port + } t.Run("LinuxAndWindows", func(t *testing.T) { t.Parallel() @@ -419,55 +494,98 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + t.Run("OK", func(t *testing.T) { + t.Parallel() - // Create a TCP listener on a random port that we expect to see in the - // response. - l, err := net.Listen("tcp", "localhost:0") - require.NoError(t, err) - defer l.Close() - tcpAddr, _ := l.Addr().(*net.TCPAddr) + client, coderdPort, agentID := setup(t, nil) - // List ports and ensure that the port we expect to see is there. - res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) - require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - var ( - expected = map[uint16]bool{ - // expect the listener we made - uint16(tcpAddr.Port): false, - // expect the coderdtest server - uint16(coderdPort): false, - } - ) - for _, port := range res.Ports { - if port.Network == codersdk.ListeningPortNetworkTCP { - if val, ok := expected[port.Port]; ok { - if val { - t.Fatalf("expected to find TCP port %d only once in response", port.Port) + // Generate a random unfiltered port. + l, lPort := generateUnfilteredPort(t) + + // List ports and ensure that the port we expect to see is there. + res, err := client.WorkspaceAgentListeningPorts(ctx, agentID) + require.NoError(t, err) + + var ( + expected = map[uint16]bool{ + // expect the listener we made + lPort: false, + // expect the coderdtest server + coderdPort: false, + } + ) + for _, port := range res.Ports { + if port.Network == codersdk.ListeningPortNetworkTCP { + if val, ok := expected[port.Port]; ok { + if val { + t.Fatalf("expected to find TCP port %d only once in response", port.Port) + } } + expected[port.Port] = true } - expected[port.Port] = true } - } - for port, found := range expected { - if !found { - t.Fatalf("expected to find TCP port %d in response", port) + for port, found := range expected { + if !found { + t.Fatalf("expected to find TCP port %d in response", port) + } } - } - // Close the listener and check that the port is no longer in the response. - require.NoError(t, l.Close()) - time.Sleep(2 * time.Second) // avoid cache - res, err = client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) - require.NoError(t, err) + // Close the listener and check that the port is no longer in the response. + require.NoError(t, l.Close()) + time.Sleep(2 * time.Second) // avoid cache + res, err = client.WorkspaceAgentListeningPorts(ctx, agentID) + require.NoError(t, err) - for _, port := range res.Ports { - if port.Network == codersdk.ListeningPortNetworkTCP && port.Port == uint16(tcpAddr.Port) { - t.Fatalf("expected to not find TCP port %d in response", tcpAddr.Port) + for _, port := range res.Ports { + if port.Network == codersdk.ListeningPortNetworkTCP && port.Port == lPort { + t.Fatalf("expected to not find TCP port %d in response", lPort) + } } - } + }) + + t.Run("Filter", func(t *testing.T) { + t.Parallel() + + // Generate an unfiltered port that we will create an app for and + // should not exist in the response. + _, appLPort := generateUnfilteredPort(t) + app := &proto.App{ + Name: "test-app", + Url: fmt.Sprintf("http://localhost:%d", appLPort), + } + + // Generate a filtered port that should not exist in the response. + _, filteredLPort := generateFilteredPort(t) + + client, coderdPort, agentID := setup(t, []*proto.App{app}) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + res, err := client.WorkspaceAgentListeningPorts(ctx, agentID) + require.NoError(t, err) + + sawCoderdPort := false + for _, port := range res.Ports { + if port.Network == codersdk.ListeningPortNetworkTCP { + if port.Port == appLPort { + t.Fatalf("expected to not find TCP port (app port) %d in response", appLPort) + } + if port.Port == filteredLPort { + t.Fatalf("expected to not find TCP port (filtered port) %d in response", filteredLPort) + } + if port.Port == coderdPort { + sawCoderdPort = true + } + } + } + if !sawCoderdPort { + t.Fatalf("expected to find TCP port (coderd port) %d in response", coderdPort) + } + }) }) t.Run("Darwin", func(t *testing.T) { @@ -477,6 +595,8 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } + client, _, agentID := setup(t, nil) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -486,7 +606,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { defer l.Close() // List ports and ensure that the list is empty because we're on darwin. - res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) + res, err := client.WorkspaceAgentListeningPorts(ctx, agentID) require.NoError(t, err) require.Len(t, res.Ports, 0) }) diff --git a/codersdk/agentconn.go b/codersdk/agentconn.go index 02d9f89d1a407..b11c440ce3a65 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -9,7 +9,9 @@ import ( "net" "net/http" "net/netip" + "os" "strconv" + "strings" "time" "golang.org/x/crypto/ssh" @@ -45,6 +47,76 @@ var ( MinimumListeningPort = 9 ) +// IgnoredListeningPorts contains a list of ports in the global ignore list. +// This list contains common TCP ports that are not HTTP servers, such as +// databases, SSH, FTP, etc. +// +// This is implemented as a map for fast lookup. +var IgnoredListeningPorts = map[uint16]struct{}{ + 0: {}, + // Ports 1-8 are reserved for future use by the Coder agent. + 1: {}, + 2: {}, + 3: {}, + 4: {}, + 5: {}, + 6: {}, + 7: {}, + 8: {}, + // ftp + 20: {}, + 21: {}, + // ssh + 22: {}, + // telnet + 23: {}, + // smtp + 25: {}, + // dns over TCP + 53: {}, + // pop3 + 110: {}, + // imap + 143: {}, + // bgp + 179: {}, + // ldap + 389: {}, + 636: {}, + // smtps + 465: {}, + // smtp + 587: {}, + // ftps + 989: {}, + 990: {}, + // imaps + 993: {}, + // pop3s + 995: {}, + // mysql + 3306: {}, + // rdp + 3389: {}, + // postgres + 5432: {}, + // mongodb + 27017: {}, + 27018: {}, + 27019: {}, + 28017: {}, +} + +func init() { + // Add a thousand more ports to the ignore list during tests so it's easier + // to find an available port. + if strings.HasSuffix(os.Args[0], ".test") { + for i := 63000; i < 64000; i++ { + IgnoredListeningPorts[uint16(i)] = struct{}{} + } + } +} + // ReconnectingPTYRequest is sent from the client to the server // to pipe data to a PTY. // @typescript-ignore ReconnectingPTYRequest 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