feat: add endpoint to get listening ports in agent (#4260)

This commit is contained in:
Dean Sheather
2022-10-06 22:38:22 +10:00
committed by GitHub
parent bbe2baf3f6
commit 1386465631
15 changed files with 501 additions and 1 deletions
+28
View File
@@ -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.
+65
View File
@@ -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
}
+13
View File
@@ -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
}
+49
View File
@@ -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,
})
}