From ed21d4aed42232f82f8dd3e3159e3855e0cf0c06 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 29 Sep 2022 11:44:44 +0000 Subject: [PATCH 01/12] feat: add endpoint to get listening ports in agent --- agent/agent.go | 27 +++++++++ agent/statsendpoint.go | 95 +++++++++++++++++++++++++++++++ coderd/coderd.go | 1 + coderd/workspaceagents.go | 31 ++++++++++ coderd/workspaceagents_test.go | 100 +++++++++++++++++++++++++++++++++ coderd/workspaceapps.go | 22 ++++++++ codersdk/agentconn.go | 67 ++++++++++++++++++++++ codersdk/client.go | 2 +- codersdk/workspaceagents.go | 15 +++++ go.mod | 2 + go.sum | 2 + 11 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 agent/statsendpoint.go diff --git a/agent/agent.go b/agent/agent.go index bb55c94eecde0..e010e9614082b 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net" + "net/http" "net/netip" "os" "os/exec" @@ -206,6 +207,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { go a.sshServer.HandleConn(a.stats.wrapConn(conn)) } }() + reconnectingPTYListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort)) if err != nil { a.logger.Critical(ctx, "listen for reconnecting pty", slog.Error(err)) @@ -240,6 +242,7 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { go a.handleReconnectingPTY(ctx, msg, conn) } }() + speedtestListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSpeedtestPort)) if err != nil { a.logger.Critical(ctx, "listen for speedtest", slog.Error(err)) @@ -261,6 +264,30 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { }() } }() + + statisticsListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetStatisticsPort)) + if err != nil { + a.logger.Critical(ctx, "listen for statistics", slog.Error(err)) + return + } + go func() { + defer statisticsListener.Close() + server := &http.Server{ + Handler: a.statisticsHandler(), + ReadTimeout: 20 * time.Second, + WriteTimeout: 20 * time.Second, + ErrorLog: slog.Stdlib(ctx, a.logger.Named("statistics_http_server"), slog.LevelInfo), + } + go func() { + <-ctx.Done() + _ = server.Close() + }() + + err = server.Serve(statisticsListener) + if err != nil && !xerrors.Is(err, http.ErrServerClosed) && !strings.Contains(err.Error(), "use of closed network connection") { + a.logger.Critical(ctx, "serve statistics HTTP server", slog.Error(err)) + } + }() } // runCoordinator listens for nodes and updates the self-node as it changes. diff --git a/agent/statsendpoint.go b/agent/statsendpoint.go new file mode 100644 index 0000000000000..939d613ed5c92 --- /dev/null +++ b/agent/statsendpoint.go @@ -0,0 +1,95 @@ +package agent + +import ( + "net/http" + "runtime" + "sync" + "time" + + "github.com/cakturk/go-netstat/netstat" + "github.com/go-chi/chi" + "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/codersdk" +) + +func (*agent) statisticsHandler() http.Handler { + r := chi.NewRouter() + r.Get("/", func(rw http.ResponseWriter, r *http.Request) { + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{ + Message: "Hello from the agent!", + }) + }) + + lp := &listeningPortsHandler{} + r.Get("/api/v0/listening-ports", lp.handler) + + return r +} + +type listeningPortsHandler struct { + mut sync.Mutex + ports []codersdk.ListeningPort + mtime time.Time +} + +func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { + lp.mut.Lock() + defer lp.mut.Unlock() + + if runtime.GOOS != "linux" && runtime.GOOS != "windows" { + // Can't scan for ports on non-linux or non-windows systems at the + // moment. The UI will not show any "no ports found" message to the + // user, so the user won't suspect a thing. + return []codersdk.ListeningPort{}, nil + } + + if time.Since(lp.mtime) < time.Second { + // copy + ports := make([]codersdk.ListeningPort, len(lp.ports)) + copy(ports, lp.ports) + return ports, nil + } + + tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { + return s.State == netstat.Listen + }) + if err != nil { + return nil, xerrors.Errorf("scan listening ports: %w", err) + } + + ports := []codersdk.ListeningPort{} + for _, tab := range tabs { + ports = append(ports, codersdk.ListeningPort{ + ProcessName: tab.Process.Name, + Network: codersdk.ListeningPortNetworkTCP, + Port: tab.LocalAddr.Port, + }) + } + + lp.ports = ports + lp.mtime = time.Now() + + // copy + ports = make([]codersdk.ListeningPort, len(lp.ports)) + copy(ports, lp.ports) + return ports, nil +} + +// handler returns a list of listening ports. This is tested by coderd's +// TestWorkspaceAgentListeningPorts test. +func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request) { + ports, err := lp.getListeningPorts() + if err != nil { + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not scan for listening ports.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ListeningPortsResponse{ + Ports: ports, + }) +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 0ac4a7c68dc5f..e9aecc4f4fa3f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -430,6 +430,7 @@ func New(options *Options) *API { ) r.Get("/", api.workspaceAgent) r.Get("/pty", api.workspaceAgentPTY) + r.Get("/listening-ports", api.workspaceAgentListeningPorts) r.Get("/connection", api.workspaceAgentConnection) r.Get("/coordinate", api.workspaceAgentClientCoordinate) // TODO: This can be removed in October. It allows for a friendly diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 180de3654e94e..b0faaa5cfa508 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -218,6 +218,37 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { _, _ = io.Copy(ptNetConn, wsNetConn) } +func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + workspace := httpmw.WorkspaceParam(r) + workspaceAgent := httpmw.WorkspaceAgentParam(r) + if !api.Authorize(r, rbac.ActionRead, workspace) { + httpapi.ResourceNotFound(rw) + return + } + + agentConn, release, err := api.workspaceAgentCache.Acquire(r, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error dialing workspace agent.", + Detail: err.Error(), + }) + return + } + defer release() + + portsResponse, err := agentConn.ListeningPorts(ctx) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching listening ports.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, portsResponse) +} + func (api *API) dialWorkspaceAgentTailnet(r *http.Request, agentID uuid.UUID) (*codersdk.AgentConn, error) { clientConn, serverConn := net.Pipe() go func() { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index d0ca0e897781a..6c3566f9d8863 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -4,7 +4,9 @@ import ( "bufio" "context" "encoding/json" + "net" "runtime" + "strconv" "strings" "testing" "time" @@ -364,6 +366,104 @@ func TestWorkspaceAgentPTY(t *testing.T) { expectLine(matchEchoOutput) } +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, + }, + }}, + }}, + }, + }, + }}, + }) + 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), + }) + defer func() { + _ = agentCloser.Close() + }() + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // 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) + + // 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) + + 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) + } + } + expected[port.Port] = true + } + } + 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) + + 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) + } + } +} + func TestWorkspaceAgentAppHealth(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 9799966a6b562..97472b1d71c6a 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strconv" "strings" "time" @@ -465,6 +466,27 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res return } + // Verify that the port is allowed. See `codersdk.MinimumListeningPort` for + // more details. + port := appURL.Port() + if port != "" { + portInt, err := strconv.Atoi(port) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: fmt.Sprintf("App URL %q has an invalid port %q. Named ports are currently not supported.", internalURL, port), + Detail: err.Error(), + }) + return + } + + if portInt < codersdk.MinimumListeningPort { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Application port %d is not permitted. Coder reserves ports less than %d for internal use.", portInt, codersdk.MinimumListeningPort), + }) + return + } + } + // Ensure path and query parameter correctness. if proxyApp.Path == "" { // Web applications typically request paths relative to the diff --git a/codersdk/agentconn.go b/codersdk/agentconn.go index f6c4da47b166c..dd53c4beb6ffe 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/json" "net" + "net/http" "net/netip" "strconv" "time" @@ -26,6 +27,16 @@ var ( TailnetSSHPort = 1 TailnetReconnectingPTYPort = 2 TailnetSpeedtestPort = 3 + // TailnetStatisticsPort serves a HTTP server with endpoints for gathering + // agent statistics. + TailnetStatisticsPort = 4 + + // MinimumListeningPort is the minimum port that the listening-ports + // endpoint will return to the client, and the minimum port that is accepted + // by the proxy applications endpoint. Coder consumes ports 1-4 at the + // moment, and we reserve some extra ports for future use. Port 9 and up are + // available for the user. + MinimumListeningPort = 9 ) // ReconnectingPTYRequest is sent from the client to the server @@ -153,3 +164,59 @@ func (c *AgentConn) DialContext(ctx context.Context, network string, addr string } return c.Conn.DialContextTCP(ctx, ipp) } + +func (c *AgentConn) statisticsClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // Disable keep alives as we're usually only making a single + // request, and this triggers goleak in tests + DisableKeepAlives: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, uint16(TailnetStatisticsPort))) + if err != nil { + return nil, xerrors.Errorf("dial statistics: %w", err) + } + + return conn, nil + }, + }, + } +} + +type ListeningPortsResponse struct { + // If there are no ports in the list, nothing should be displayed in the UI. + // There must not be a "no ports available" message or anything similar, as + // there will always be no ports displayed on platforms where our port + // detection logic is unsupported. + Ports []ListeningPort `json:"ports"` +} + +type ListeningPortNetwork string + +const ( + ListeningPortNetworkTCP ListeningPortNetwork = "tcp" +) + +type ListeningPort struct { + ProcessName string `json:"process_name"` + Network ListeningPortNetwork `json:"network"` // only "tcp" at the moment + Port uint16 `json:"port"` +} + +func (c *AgentConn) ListeningPorts(ctx context.Context) (ListeningPortsResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://agent-stats/api/v0/listening-ports", nil) + if err != nil { + return ListeningPortsResponse{}, xerrors.Errorf("new request: %w", err) + } + res, err := c.statisticsClient().Do(req) + if err != nil { + return ListeningPortsResponse{}, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ListeningPortsResponse{}, readBodyAsError(res) + } + + var resp ListeningPortsResponse + return resp, json.NewDecoder(res.Body).Decode(&resp) +} diff --git a/codersdk/client.go b/codersdk/client.go index 3c0e6a7b0d3ce..63c6304721316 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -16,7 +16,7 @@ import ( // These cookies are Coder-specific. If a new one is added or changed, the name // shouldn't be likely to conflict with any user-application set cookies. -// Be sure to strip additional cookies in httpapi.StripCoder Cookies! +// Be sure to strip additional cookies in httpapi.StripCoderCookies! const ( // SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. SessionTokenKey = "coder_session_token" diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 48c5743b7f894..e29a564d281b0 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -469,6 +469,21 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } +// WorkspaceAgentListeningPorts returns a list of ports that are currently being +// listened on inside the workspace agent's network namespace. +func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid.UUID) (ListeningPortsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s/listening-ports", agentID), nil) + if err != nil { + return ListeningPortsResponse{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return ListeningPortsResponse{}, readBodyAsError(res) + } + var listeningPorts ListeningPortsResponse + return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts) +} + // Stats records the Agent's network connection statistics for use in // user-facing metrics and debugging. // Each member value must be written and read with atomic. diff --git a/go.mod b/go.mod index 0870322f9c4d0..546b211e473b7 100644 --- a/go.mod +++ b/go.mod @@ -156,6 +156,8 @@ require ( tailscale.com v1.30.0 ) +require github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 // indirect + require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect diff --git a/go.sum b/go.sum index a0cd57f00ff08..2752b974cda4a 100644 --- a/go.sum +++ b/go.sum @@ -282,6 +282,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/bytecodealliance/wasmtime-go v0.36.0 h1:B6thr7RMM9xQmouBtUqm1RpkJjuLS37m6nxX+iwsQSc= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= From d4663fad33fb3e987115f9e8fb82ef53fbd7e100 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 29 Sep 2022 12:01:45 +0000 Subject: [PATCH 02/12] fixup! feat: add endpoint to get listening ports in agent --- agent/statsendpoint.go | 4 ++++ coderd/workspaceapps.go | 8 ++++---- coderd/workspaceapps_test.go | 19 +++++++++++++++++++ codersdk/agentconn.go | 4 ++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/agent/statsendpoint.go b/agent/statsendpoint.go index 939d613ed5c92..c2ca275f199a7 100644 --- a/agent/statsendpoint.go +++ b/agent/statsendpoint.go @@ -61,6 +61,10 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, ports := []codersdk.ListeningPort{} for _, tab := range tabs { + if tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { + continue + } + ports = append(ports, codersdk.ListeningPort{ ProcessName: tab.Process.Name, Network: codersdk.ListeningPortNetworkTCP, diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 97472b1d71c6a..10f73dcd6112f 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -466,14 +466,14 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res return } - // Verify that the port is allowed. See `codersdk.MinimumListeningPort` for - // more details. + // Verify that the port is allowed. See the docs above + // `codersdk.MinimumListeningPort` for more details. port := appURL.Port() if port != "" { portInt, err := strconv.Atoi(port) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("App URL %q has an invalid port %q. Named ports are currently not supported.", internalURL, port), + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("App URL %q has an invalid port %q.", internalURL, port), Detail: err.Error(), }) return diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index e111863f578df..b80865e6323a5 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -695,4 +695,23 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) { defer resp.Body.Close() require.Equal(t, http.StatusBadGateway, resp.StatusCode) }) + + t.Run("ProxyPortMinimumError", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + port := uint16(codersdk.MinimumListeningPort - 1) + resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, port, "/", proxyTestAppQuery), nil) + require.NoError(t, err) + defer resp.Body.Close() + + // Should have an error response. + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + var resBody codersdk.Response + err = json.NewDecoder(resp.Body).Decode(&resBody) + require.NoError(t, err) + require.Contains(t, resBody.Message, "Coder reserves ports less than") + }) } diff --git a/codersdk/agentconn.go b/codersdk/agentconn.go index dd53c4beb6ffe..7d76eb1fdc69a 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -36,6 +36,10 @@ var ( // by the proxy applications endpoint. Coder consumes ports 1-4 at the // moment, and we reserve some extra ports for future use. Port 9 and up are // available for the user. + // + // This is not enforced in the CLI intentionally as we don't really care + // *that* much. The user could bypass this in the CLI by using SSH instead + // anyways. MinimumListeningPort = 9 ) From 3f741c6b7b5aee421ed334dfc89bd0d299862189 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 29 Sep 2022 12:07:45 +0000 Subject: [PATCH 03/12] fixup! feat: add endpoint to get listening ports in agent --- site/src/api/typesGenerated.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fdacbc6dacadf..7c3f2f08335d8 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -287,6 +287,18 @@ export interface License { readonly claims: Record } +// From codersdk/agentconn.go +export interface ListeningPort { + readonly process_name: string + readonly network: ListeningPortNetwork + readonly port: number +} + +// From codersdk/agentconn.go +export interface ListeningPortsResponse { + readonly ports: ListeningPort[] +} + // From codersdk/users.go export interface LoginWithPasswordRequest { readonly email: string @@ -671,6 +683,9 @@ export type BuildReason = "autostart" | "autostop" | "initiator" // From codersdk/features.go export type Entitlement = "entitled" | "grace_period" | "not_entitled" +// From codersdk/agentconn.go +export type ListeningPortNetwork = "tcp" + // From codersdk/provisionerdaemons.go export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" From 4fbc0ffeaaefaefa9c321cf59ab7b0c31f9f97dd Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 4 Oct 2022 15:38:51 +0000 Subject: [PATCH 04/12] fixup! feat: add endpoint to get listening ports in agent --- codersdk/agentconn.go | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/codersdk/agentconn.go b/codersdk/agentconn.go index 7d76eb1fdc69a..2279cc52b22be 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -4,6 +4,8 @@ import ( "context" "encoding/binary" "encoding/json" + "fmt" + "io" "net" "net/http" "net/netip" @@ -176,6 +178,19 @@ func (c *AgentConn) statisticsClient() *http.Client { // request, and this triggers goleak in tests DisableKeepAlives: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if network != "tcp" { + return nil, xerrors.Errorf("network must be tcp") + } + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, xerrors.Errorf("split host port %q: %w", addr, err) + } + // Verify that host is TailnetIP and port is + // TailnetStatisticsPort. + if host != TailnetIP.String() || port != strconv.Itoa(TailnetStatisticsPort) { + return nil, xerrors.Errorf("request %q does not appear to be for statistics server", addr) + } + conn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(TailnetIP, uint16(TailnetStatisticsPort))) if err != nil { return nil, xerrors.Errorf("dial statistics: %w", err) @@ -187,6 +202,18 @@ func (c *AgentConn) statisticsClient() *http.Client { } } +func (c *AgentConn) doStatisticsRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + host := net.JoinHostPort(TailnetIP.String(), strconv.Itoa(TailnetStatisticsPort)) + url := fmt.Sprintf("http://%s%s", host, path) + + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, xerrors.Errorf("new statistics server request to %q: %w", url, err) + } + + return c.statisticsClient().Do(req) +} + type ListeningPortsResponse struct { // If there are no ports in the list, nothing should be displayed in the UI. // There must not be a "no ports available" message or anything similar, as @@ -208,11 +235,7 @@ type ListeningPort struct { } func (c *AgentConn) ListeningPorts(ctx context.Context) (ListeningPortsResponse, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://agent-stats/api/v0/listening-ports", nil) - if err != nil { - return ListeningPortsResponse{}, xerrors.Errorf("new request: %w", err) - } - res, err := c.statisticsClient().Do(req) + res, err := c.doStatisticsRequest(ctx, http.MethodGet, "/api/v0/listening-ports", nil) if err != nil { return ListeningPortsResponse{}, xerrors.Errorf("do request: %w", err) } From 0f7b8dc553116572576994e6f5f768df4d65fc1d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 4 Oct 2022 15:42:18 +0000 Subject: [PATCH 05/12] fixup! feat: add endpoint to get listening ports in agent --- agent/agent.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index e010e9614082b..6d0a9a952f44b 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -273,10 +273,11 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) { go func() { defer statisticsListener.Close() server := &http.Server{ - Handler: a.statisticsHandler(), - ReadTimeout: 20 * time.Second, - WriteTimeout: 20 * time.Second, - ErrorLog: slog.Stdlib(ctx, a.logger.Named("statistics_http_server"), slog.LevelInfo), + Handler: a.statisticsHandler(), + ReadTimeout: 20 * time.Second, + ReadHeaderTimeout: 20 * time.Second, + WriteTimeout: 20 * time.Second, + ErrorLog: slog.Stdlib(ctx, a.logger.Named("statistics_http_server"), slog.LevelInfo), } go func() { <-ctx.Done() From ea2027e9539f38664cdea341b8af375417ae8ff5 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 4 Oct 2022 16:14:58 +0000 Subject: [PATCH 06/12] fixup! feat: add endpoint to get listening ports in agent --- agent/ports_supported.go | 61 ++++++++++++++++++ agent/ports_unsupported.go | 13 ++++ agent/statsendpoint.go | 50 --------------- coderd/workspaceagents_test.go | 111 +++++++++++++++++++++------------ 4 files changed, 144 insertions(+), 91 deletions(-) create mode 100644 agent/ports_supported.go create mode 100644 agent/ports_unsupported.go diff --git a/agent/ports_supported.go b/agent/ports_supported.go new file mode 100644 index 0000000000000..bf7051205cb2c --- /dev/null +++ b/agent/ports_supported.go @@ -0,0 +1,61 @@ +//go:build linux || windows +// +build linux windows + +package agent + +import ( + "runtime" + "time" + + "github.com/cakturk/go-netstat/netstat" + "golang.org/x/xerrors" + + "github.com/coder/coder/codersdk" +) + +func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { + lp.mut.Lock() + defer lp.mut.Unlock() + + if runtime.GOOS != "linux" && runtime.GOOS != "windows" { + // Can't scan for ports on non-linux or non-windows systems at the + // moment. The UI will not show any "no ports found" message to the + // user, so the user won't suspect a thing. + return []codersdk.ListeningPort{}, nil + } + + if time.Since(lp.mtime) < time.Second { + // copy + ports := make([]codersdk.ListeningPort, len(lp.ports)) + copy(ports, lp.ports) + return ports, nil + } + + tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { + return s.State == netstat.Listen + }) + if err != nil { + return nil, xerrors.Errorf("scan listening ports: %w", err) + } + + ports := []codersdk.ListeningPort{} + for _, tab := range tabs { + if tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { + continue + } + + ports = append(ports, codersdk.ListeningPort{ + ProcessName: tab.Process.Name, + Network: codersdk.ListeningPortNetworkTCP, + Port: tab.LocalAddr.Port, + }) + } + + lp.ports = ports + lp.mtime = time.Now() + + // copy + ports = make([]codersdk.ListeningPort, len(lp.ports)) + copy(ports, lp.ports) + return ports, nil +} diff --git a/agent/ports_unsupported.go b/agent/ports_unsupported.go new file mode 100644 index 0000000000000..2eabdaca330ac --- /dev/null +++ b/agent/ports_unsupported.go @@ -0,0 +1,13 @@ +//go:build !linux && !windows +// +build !linux,!windows + +package agent + +import "github.com/coder/coder/codersdk" + +func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { + // Can't scan for ports on non-linux or non-windows systems at the moment. + // The UI will not show any "no ports found" message to the user, so the + // user won't suspect a thing. + return []codersdk.ListeningPort{}, nil +} diff --git a/agent/statsendpoint.go b/agent/statsendpoint.go index c2ca275f199a7..0ddc01f70ddb5 100644 --- a/agent/statsendpoint.go +++ b/agent/statsendpoint.go @@ -2,13 +2,10 @@ package agent import ( "net/http" - "runtime" "sync" "time" - "github.com/cakturk/go-netstat/netstat" "github.com/go-chi/chi" - "golang.org/x/xerrors" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/codersdk" @@ -34,53 +31,6 @@ type listeningPortsHandler struct { mtime time.Time } -func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, error) { - lp.mut.Lock() - defer lp.mut.Unlock() - - if runtime.GOOS != "linux" && runtime.GOOS != "windows" { - // Can't scan for ports on non-linux or non-windows systems at the - // moment. The UI will not show any "no ports found" message to the - // user, so the user won't suspect a thing. - return []codersdk.ListeningPort{}, nil - } - - if time.Since(lp.mtime) < time.Second { - // copy - ports := make([]codersdk.ListeningPort, len(lp.ports)) - copy(ports, lp.ports) - return ports, nil - } - - tabs, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { - return s.State == netstat.Listen - }) - if err != nil { - return nil, xerrors.Errorf("scan listening ports: %w", err) - } - - ports := []codersdk.ListeningPort{} - for _, tab := range tabs { - if tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { - continue - } - - ports = append(ports, codersdk.ListeningPort{ - ProcessName: tab.Process.Name, - Network: codersdk.ListeningPortNetworkTCP, - Port: tab.LocalAddr.Port, - }) - } - - lp.ports = ports - lp.mtime = time.Now() - - // copy - ports = make([]codersdk.ListeningPort, len(lp.ports)) - copy(ports, lp.ports) - return ports, nil -} - // handler returns a list of listening ports. This is tested by coderd's // TestWorkspaceAgentListeningPorts test. func (lp *listeningPortsHandler) handler(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 6c3566f9d8863..7cfebdf5166e0 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -408,60 +408,89 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet, Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug), }) - defer func() { + t.Cleanup(func() { _ = agentCloser.Close() - }() + }) resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + t.Run("LinuxAndWindows", func(t *testing.T) { + t.Parallel() + if runtime.GOOS != "linux" && runtime.GOOS != "windows" { + t.Skip("only runs on linux and windows") + return + } - // 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) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - // 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) + // 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) - 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) + // 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) + + 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) + } } + 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, resources[0].Agents[0].ID) + 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 == uint16(tcpAddr.Port) { + t.Fatalf("expected to not find TCP port %d in response", tcpAddr.Port) + } } - } + }) + + t.Run("Darwin", func(t *testing.T) { + t.Parallel() + if runtime.GOOS != "darwin" { + t.Skip("only runs on darwin") + return + } + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Create a TCP listener on a random port. + l, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + 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) + require.NoError(t, err) + require.Len(t, res.Ports, 0) + }) } func TestWorkspaceAgentAppHealth(t *testing.T) { From e2973bad6b262d10860bbc993b3fa7b26738e604 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 4 Oct 2022 17:51:02 +0000 Subject: [PATCH 07/12] fixup! feat: add endpoint to get listening ports in agent --- agent/ports_supported.go | 8 -------- coderd/workspaceagents.go | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index bf7051205cb2c..ba7fa7aa374da 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -4,7 +4,6 @@ package agent import ( - "runtime" "time" "github.com/cakturk/go-netstat/netstat" @@ -17,13 +16,6 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, lp.mut.Lock() defer lp.mut.Unlock() - if runtime.GOOS != "linux" && runtime.GOOS != "windows" { - // Can't scan for ports on non-linux or non-windows systems at the - // moment. The UI will not show any "no ports found" message to the - // user, so the user won't suspect a thing. - return []codersdk.ListeningPort{}, nil - } - if time.Since(lp.mtime) < time.Second { // copy ports := make([]codersdk.ListeningPort, len(lp.ports)) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index b0faaa5cfa508..055f02802d388 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -227,6 +227,21 @@ func (api *API) workspaceAgentListeningPorts(rw http.ResponseWriter, r *http.Req return } + apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, workspaceAgent, nil, api.AgentInactiveDisconnectTimeout) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error reading workspace agent.", + Detail: err.Error(), + }) + return + } + if apiAgent.Status != codersdk.WorkspaceAgentConnected { + httpapi.Write(ctx, rw, http.StatusPreconditionRequired, codersdk.Response{ + Message: fmt.Sprintf("Agent state is %q, it must be in the %q state.", apiAgent.Status, codersdk.WorkspaceAgentConnected), + }) + return + } + agentConn, release, err := api.workspaceAgentCache.Acquire(r, workspaceAgent.ID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ From d9635a88dc9363c2611d57b53df88d6976b73125 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 11:40:38 +0000 Subject: [PATCH 08/12] please work tests --- coderd/workspaceagents_test.go | 97 +++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 7cfebdf5166e0..d7018201714fc 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -368,50 +368,55 @@ 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) (client *codersdk.Client, agentID uuid.UUID, coderdPort uint16) { + client = coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + coderdPortInt, 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, + }, + }}, }}, - }}, + }, }, - }, - }}, - }) - 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.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.LatestBuild.ID) + + return client, resources[0].Agents[0].ID, uint16(coderdPortInt) + } t.Run("LinuxAndWindows", func(t *testing.T) { t.Parallel() @@ -420,6 +425,8 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } + client, agentID, coderdPort := setup(t) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -431,7 +438,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { tcpAddr, _ := l.Addr().(*net.TCPAddr) // List ports and ensure that the port we expect to see is there. - res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) + res, err := client.WorkspaceAgentListeningPorts(ctx, agentID) require.NoError(t, err) var ( @@ -439,7 +446,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // expect the listener we made uint16(tcpAddr.Port): false, // expect the coderdtest server - uint16(coderdPort): false, + coderdPort: false, } ) for _, port := range res.Ports { @@ -461,7 +468,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // 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) + res, err = client.WorkspaceAgentListeningPorts(ctx, agentID) require.NoError(t, err) for _, port := range res.Ports { @@ -478,6 +485,8 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } + client, agentID, _ := setup(t) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -487,7 +496,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) }) From 39626354913c7e97003f04ee8bbb8ad89d9a0cba Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 11:47:01 +0000 Subject: [PATCH 09/12] Revert "please work tests" This reverts commit d9635a88dc9363c2611d57b53df88d6976b73125. --- coderd/workspaceagents_test.go | 97 +++++++++++++++------------------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index d7018201714fc..7cfebdf5166e0 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -368,55 +368,50 @@ 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) - setup := func(t *testing.T) (client *codersdk.Client, agentID uuid.UUID, coderdPort uint16) { - client = coderdtest.New(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }) - coderdPortInt, 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, - }, - }}, + 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, + }, }}, - }, + }}, }, - }}, - }) - 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.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) - return client, resources[0].Agents[0].ID, uint16(coderdPortInt) - } + 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.LatestBuild.ID) t.Run("LinuxAndWindows", func(t *testing.T) { t.Parallel() @@ -425,8 +420,6 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } - client, agentID, coderdPort := setup(t) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -438,7 +431,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { tcpAddr, _ := l.Addr().(*net.TCPAddr) // List ports and ensure that the port we expect to see is there. - res, err := client.WorkspaceAgentListeningPorts(ctx, agentID) + res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) require.NoError(t, err) var ( @@ -446,7 +439,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // expect the listener we made uint16(tcpAddr.Port): false, // expect the coderdtest server - coderdPort: false, + uint16(coderdPort): false, } ) for _, port := range res.Ports { @@ -468,7 +461,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { // 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) + res, err = client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) require.NoError(t, err) for _, port := range res.Ports { @@ -485,8 +478,6 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { return } - client, agentID, _ := setup(t) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() @@ -496,7 +487,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, agentID) + res, err := client.WorkspaceAgentListeningPorts(ctx, resources[0].Agents[0].ID) require.NoError(t, err) require.Len(t, res.Ports, 0) }) From d4b7ce957c8b4dfcf10fc9ee5db7873696251fdf Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 11:48:10 +0000 Subject: [PATCH 10/12] fixup! Merge branch 'main' into dean/listening-ports --- coderd/workspaceagents_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index dc47fb37d6a53..e4bbe42a5af6d 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -410,7 +410,7 @@ func TestWorkspaceAgentListeningPorts(t *testing.T) { t.Cleanup(func() { _ = agentCloser.Close() }) - resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) t.Run("LinuxAndWindows", func(t *testing.T) { t.Parallel() From b084277d37637e67bb99f406d9d0ba590123ecfe Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 12:02:16 +0000 Subject: [PATCH 11/12] fixup! Merge branch 'main' into dean/listening-ports --- agent/ports_supported.go | 8 ++++++-- codersdk/agentconn.go | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index ba7fa7aa374da..4d3351996a7f7 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -32,12 +32,16 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, ports := []codersdk.ListeningPort{} for _, tab := range tabs { - if tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { + if tab.LocalAddr == nil || tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { continue } + procName := "" + if tab.Process != nil { + procName = tab.Process.Name + } ports = append(ports, codersdk.ListeningPort{ - ProcessName: tab.Process.Name, + ProcessName: procName, Network: codersdk.ListeningPortNetworkTCP, Port: tab.LocalAddr.Port, }) diff --git a/codersdk/agentconn.go b/codersdk/agentconn.go index 2279cc52b22be..02d9f89d1a407 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -229,8 +229,8 @@ const ( ) type ListeningPort struct { - ProcessName string `json:"process_name"` - Network ListeningPortNetwork `json:"network"` // only "tcp" at the moment + ProcessName string `json:"process_name"` // may be empty + Network ListeningPortNetwork `json:"network"` // only "tcp" at the moment Port uint16 `json:"port"` } From 0960944e1d69a5de9caa3192d5425dbe42c0262d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 6 Oct 2022 12:20:08 +0000 Subject: [PATCH 12/12] fixup! Merge branch 'main' into dean/listening-ports --- agent/ports_supported.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agent/ports_supported.go b/agent/ports_supported.go index 4d3351996a7f7..e405aa6c1bbc1 100644 --- a/agent/ports_supported.go +++ b/agent/ports_supported.go @@ -30,12 +30,20 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort, return nil, xerrors.Errorf("scan listening ports: %w", err) } + seen := make(map[uint16]struct{}, len(tabs)) ports := []codersdk.ListeningPort{} for _, tab := range tabs { if tab.LocalAddr == nil || tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) { continue } + // Don't include ports that we've already seen. This can happen on + // Windows, and maybe on Linux if you're using a shared listener socket. + if _, ok := seen[tab.LocalAddr.Port]; ok { + continue + } + seen[tab.LocalAddr.Port] = struct{}{} + procName := "" if tab.Process != nil { procName = tab.Process.Name 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