mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add endpoint to get listening ports in agent (#4260)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user