Skip to content

Commit d1496ed

Browse files
committed
feat: add endpoint for netstat web socket
1 parent e9a28be commit d1496ed

File tree

5 files changed

+149
-0
lines changed

5 files changed

+149
-0
lines changed

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ func New(options *Options) *API {
285285
r.Get("/", api.workspaceAgent)
286286
r.Get("/dial", api.workspaceAgentDial)
287287
r.Get("/turn", api.workspaceAgentTurn)
288+
r.Get("/netstat", api.workspaceAgentNetstat)
288289
r.Get("/pty", api.workspaceAgentPTY)
289290
r.Get("/iceservers", api.workspaceAgentICEServers)
290291
})

coderd/coderd_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
139139
"GET:/api/v2/workspaceagents/{workspaceagent}": {NoAuthorize: true},
140140
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {NoAuthorize: true},
141141
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
142+
"GET:/api/v2/workspaceagents/{workspaceagent}/netstat": {NoAuthorize: true},
142143
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
143144
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
144145

coderd/workspaceagents.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,55 @@ func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency
501501

502502
return workspaceAgent, nil
503503
}
504+
505+
// workspaceAgentNetstat sends listening ports as `agent.NetstatResponse` on an
506+
// interval.
507+
func (api *API) workspaceAgentNetstat(rw http.ResponseWriter, r *http.Request) {
508+
api.websocketWaitMutex.Lock()
509+
api.websocketWaitGroup.Add(1)
510+
api.websocketWaitMutex.Unlock()
511+
defer api.websocketWaitGroup.Done()
512+
513+
workspaceAgent := httpmw.WorkspaceAgentParam(r)
514+
apiAgent, err := convertWorkspaceAgent(workspaceAgent, api.AgentConnectionUpdateFrequency)
515+
if err != nil {
516+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
517+
Message: fmt.Sprintf("convert workspace agent: %s", err),
518+
})
519+
return
520+
}
521+
if apiAgent.Status != codersdk.WorkspaceAgentConnected {
522+
httpapi.Write(rw, http.StatusPreconditionRequired, httpapi.Response{
523+
Message: fmt.Sprintf("agent must be in the connected state: %s", apiAgent.Status),
524+
})
525+
return
526+
}
527+
528+
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
529+
CompressionMode: websocket.CompressionDisabled,
530+
})
531+
if err != nil {
532+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
533+
Message: fmt.Sprintf("accept websocket: %s", err),
534+
})
535+
return
536+
}
537+
defer func() {
538+
_ = conn.Close(websocket.StatusNormalClosure, "ended")
539+
}()
540+
wsNetConn := websocket.NetConn(r.Context(), conn, websocket.MessageBinary)
541+
agentConn, err := api.dialWorkspaceAgent(r, workspaceAgent.ID)
542+
if err != nil {
543+
_ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial workspace agent: %s", err))
544+
return
545+
}
546+
defer agentConn.Close()
547+
ptNetConn, err := agentConn.Netstat(r.Context())
548+
if err != nil {
549+
_ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("dial: %s", err))
550+
return
551+
}
552+
defer ptNetConn.Close()
553+
554+
agent.Bicopy(r.Context(), wsNetConn, ptNetConn)
555+
}

coderd/workspaceagents_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"context"
66
"encoding/json"
7+
"fmt"
78
"runtime"
89
"strings"
910
"testing"
@@ -264,3 +265,67 @@ func TestWorkspaceAgentPTY(t *testing.T) {
264265
expectLine(matchEchoCommand)
265266
expectLine(matchEchoOutput)
266267
}
268+
269+
func TestWorkspaceAgentNetstat(t *testing.T) {
270+
t.Parallel()
271+
272+
client, coderAPI := coderdtest.NewWithAPI(t, nil)
273+
user := coderdtest.CreateFirstUser(t, client)
274+
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
275+
authToken := uuid.NewString()
276+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
277+
Parse: echo.ParseComplete,
278+
ProvisionDryRun: echo.ProvisionComplete,
279+
Provision: []*proto.Provision_Response{{
280+
Type: &proto.Provision_Response_Complete{
281+
Complete: &proto.Provision_Complete{
282+
Resources: []*proto.Resource{{
283+
Name: "example",
284+
Type: "aws_instance",
285+
Agents: []*proto.Agent{{
286+
Id: uuid.NewString(),
287+
Auth: &proto.Agent_Token{
288+
Token: authToken,
289+
},
290+
}},
291+
}},
292+
},
293+
},
294+
}},
295+
})
296+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
297+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
298+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
299+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
300+
daemonCloser.Close()
301+
302+
agentClient := codersdk.New(client.URL)
303+
agentClient.SessionToken = authToken
304+
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{
305+
Logger: slogtest.Make(t, nil),
306+
})
307+
t.Cleanup(func() {
308+
_ = agentCloser.Close()
309+
})
310+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
311+
312+
conn, err := client.WorkspaceAgentNetstat(context.Background(), resources[0].Agents[0].ID)
313+
require.NoError(t, err)
314+
defer conn.Close()
315+
316+
decoder := json.NewDecoder(conn)
317+
318+
expectNetstat := func() {
319+
var res agent.NetstatResponse
320+
err = decoder.Decode(&res)
321+
require.NoError(t, err)
322+
323+
if runtime.GOOS == "linux" || runtime.GOOS == "windows" {
324+
require.NotNil(t, res.Ports)
325+
} else {
326+
require.Equal(t, fmt.Sprintf("Port scanning is not supported on %s", runtime.GOOS), res.Error)
327+
}
328+
}
329+
330+
expectNetstat()
331+
}

codersdk/workspaceagents.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,36 @@ func (c *Client) WorkspaceAgentReconnectingPTY(ctx context.Context, agentID, rec
369369
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
370370
}
371371

372+
// WorkspaceAgentNetstat sends listening ports as `agent.NetstatResponse` on an
373+
// interval.
374+
func (c *Client) WorkspaceAgentNetstat(ctx context.Context, agentID uuid.UUID) (net.Conn, error) {
375+
serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/netstat", agentID))
376+
if err != nil {
377+
return nil, xerrors.Errorf("parse url: %w", err)
378+
}
379+
jar, err := cookiejar.New(nil)
380+
if err != nil {
381+
return nil, xerrors.Errorf("create cookie jar: %w", err)
382+
}
383+
jar.SetCookies(serverURL, []*http.Cookie{{
384+
Name: httpmw.SessionTokenKey,
385+
Value: c.SessionToken,
386+
}})
387+
httpClient := &http.Client{
388+
Jar: jar,
389+
}
390+
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
391+
HTTPClient: httpClient,
392+
})
393+
if err != nil {
394+
if res == nil {
395+
return nil, err
396+
}
397+
return nil, readBodyAsError(res)
398+
}
399+
return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil
400+
}
401+
372402
func (c *Client) turnProxyDialer(ctx context.Context, httpClient *http.Client, path string) proxy.Dialer {
373403
return turnconn.ProxyDialer(func() (net.Conn, error) {
374404
turnURL, err := c.URL.Parse(path)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy