diff --git a/agent/agent.go b/agent/agent.go index bb55c94eecde0..6d0a9a952f44b 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,31 @@ 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, + ReadHeaderTimeout: 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/ports_supported.go b/agent/ports_supported.go new file mode 100644 index 0000000000000..e405aa6c1bbc1 --- /dev/null +++ b/agent/ports_supported.go @@ -0,0 +1,65 @@ +//go:build linux || windows +// +build linux windows + +package agent + +import ( + "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 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) + } + + 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 + } + ports = append(ports, codersdk.ListeningPort{ + ProcessName: procName, + 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 new file mode 100644 index 0000000000000..0ddc01f70ddb5 --- /dev/null +++ b/agent/statsendpoint.go @@ -0,0 +1,49 @@ +package agent + +import ( + "net/http" + "sync" + "time" + + "github.com/go-chi/chi" + + "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 +} + +// 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 011f29927d92e..ada805e19459f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -438,6 +438,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 7581bb2d216f9..f7301b9bd38eb 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -219,6 +219,52 @@ 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 + } + + 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{ + 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 f1261ba60dc1c..e4bbe42a5af6d 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" @@ -363,6 +365,133 @@ 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), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + 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 + } + + 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) + } + } + }) + + 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) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{ diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 4b0b6eb94c5d5..1e3e416861ec3 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strconv" "strings" "time" @@ -486,6 +487,27 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res return } + // 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.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("App URL %q has an invalid port %q.", 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/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index b3999b388f1c9..b1a090aba3431 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -692,4 +692,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 f6c4da47b166c..02d9f89d1a407 100644 --- a/codersdk/agentconn.go +++ b/codersdk/agentconn.go @@ -4,7 +4,10 @@ import ( "context" "encoding/binary" "encoding/json" + "fmt" + "io" "net" + "net/http" "net/netip" "strconv" "time" @@ -26,6 +29,20 @@ 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. + // + // 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 ) // ReconnectingPTYRequest is sent from the client to the server @@ -153,3 +170,80 @@ 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) { + 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) + } + + return conn, nil + }, + }, + } +} + +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 + // 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"` // may be empty + Network ListeningPortNetwork `json:"network"` // only "tcp" at the moment + Port uint16 `json:"port"` +} + +func (c *AgentConn) ListeningPorts(ctx context.Context) (ListeningPortsResponse, error) { + res, err := c.doStatisticsRequest(ctx, http.MethodGet, "/api/v0/listening-ports", nil) + 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 90216dcdd8e26..afc37396feaa5 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 8d1fdb39f76a8..253e8713fdb4f 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -520,6 +520,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 233382b3b9900..36379f3e99796 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 7f5deb81a42e2..ab899f0900601 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= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5b9117c1672e6..155d359119410 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -288,6 +288,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 @@ -680,6 +692,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"