Skip to content

Commit 8c15192

Browse files
feat(cli): add p2p diagnostics to ping (#14426)
First PR to address #14244. Adds common potential reasons as to why a direct connection to the workspace agent couldn't be established to `coder ping`: - If the Coder deployment administrator has blocked direction connections (`CODER_BLOCK_DIRECT`). - If the client has no STUN servers within it's DERP map. - If the client or agent appears to be behind a hard NAT, as per Tailscale `netInfo.MappingVariesByDestIP` Also adds a warning if the client or agent has a network interface below the 'safe' MTU for tailnet. This warning is always displayed at the end of a `coder ping`.
1 parent b36d979 commit 8c15192

File tree

8 files changed

+298
-5
lines changed

8 files changed

+298
-5
lines changed

agent/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func (a *agent) apiHandler() http.Handler {
3737
}
3838
promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger)
3939
r.Get("/api/v0/listening-ports", lp.handler)
40+
r.Get("/api/v0/netcheck", a.HandleNetcheck)
4041
r.Get("/debug/logs", a.HandleHTTPDebugLogs)
4142
r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock)
4243
r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState)

agent/health.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package agent
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/coder/coder/v2/coderd/healthcheck/health"
7+
"github.com/coder/coder/v2/coderd/httpapi"
8+
"github.com/coder/coder/v2/codersdk"
9+
"github.com/coder/coder/v2/codersdk/healthsdk"
10+
)
11+
12+
func (a *agent) HandleNetcheck(rw http.ResponseWriter, r *http.Request) {
13+
ni := a.TailnetConn().GetNetInfo()
14+
15+
ifReport, err := healthsdk.RunInterfacesReport()
16+
if err != nil {
17+
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
18+
Message: "Failed to run interfaces report",
19+
Detail: err.Error(),
20+
})
21+
return
22+
}
23+
24+
httpapi.Write(r.Context(), rw, http.StatusOK, healthsdk.AgentNetcheckReport{
25+
BaseReport: healthsdk.BaseReport{
26+
Severity: health.SeverityOK,
27+
},
28+
NetInfo: ni,
29+
Interfaces: ifReport,
30+
})
31+
}

cli/cliui/agent.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import (
1010

1111
"github.com/google/uuid"
1212
"golang.org/x/xerrors"
13+
"tailscale.com/tailcfg"
1314

1415
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/coder/v2/codersdk/healthsdk"
17+
"github.com/coder/coder/v2/codersdk/workspacesdk"
1518
"github.com/coder/coder/v2/tailnet"
1619
)
1720

@@ -346,3 +349,55 @@ func PeerDiagnostics(w io.Writer, d tailnet.PeerDiagnostics) {
346349
_, _ = fmt.Fprint(w, "✘ Wireguard is not connected\n")
347350
}
348351
}
352+
353+
type ConnDiags struct {
354+
ConnInfo *workspacesdk.AgentConnectionInfo
355+
PingP2P bool
356+
DisableDirect bool
357+
LocalNetInfo *tailcfg.NetInfo
358+
LocalInterfaces *healthsdk.InterfacesReport
359+
AgentNetcheck *healthsdk.AgentNetcheckReport
360+
// TODO: More diagnostics
361+
}
362+
363+
func ConnDiagnostics(w io.Writer, d ConnDiags) {
364+
if d.AgentNetcheck != nil {
365+
for _, msg := range d.AgentNetcheck.Interfaces.Warnings {
366+
_, _ = fmt.Fprintf(w, "❗ Agent: %s\n", msg.Message)
367+
}
368+
}
369+
370+
if d.LocalInterfaces != nil {
371+
for _, msg := range d.LocalInterfaces.Warnings {
372+
_, _ = fmt.Fprintf(w, "❗ Client: %s\n", msg.Message)
373+
}
374+
}
375+
376+
if d.PingP2P {
377+
_, _ = fmt.Fprint(w, "✔ You are connected directly (p2p)\n")
378+
return
379+
}
380+
_, _ = fmt.Fprint(w, "❗ You are connected via a DERP relay, not directly (p2p)\n")
381+
382+
if d.DisableDirect {
383+
_, _ = fmt.Fprint(w, "❗ Direct connections are disabled locally, by `--disable-direct` or `CODER_DISABLE_DIRECT`\n")
384+
return
385+
}
386+
387+
if d.ConnInfo != nil && d.ConnInfo.DisableDirectConnections {
388+
_, _ = fmt.Fprint(w, "❗ Your Coder administrator has blocked direct connections\n")
389+
return
390+
}
391+
392+
if d.ConnInfo != nil && d.ConnInfo.DERPMap != nil && !d.ConnInfo.DERPMap.HasSTUN() {
393+
_, _ = fmt.Fprint(w, "✘ The DERP map is not configured to use STUN, which will prevent direct connections from starting outside of local networks\n")
394+
}
395+
396+
if d.LocalNetInfo != nil && d.LocalNetInfo.MappingVariesByDestIP.EqualBool(true) {
397+
_, _ = fmt.Fprint(w, "❗ Client is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n")
398+
}
399+
400+
if d.AgentNetcheck != nil && d.AgentNetcheck.NetInfo != nil && d.AgentNetcheck.NetInfo.MappingVariesByDestIP.EqualBool(true) {
401+
_, _ = fmt.Fprint(w, "❗ Agent is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers\n")
402+
}
403+
}

cli/cliui/agent_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ import (
2020

2121
"github.com/coder/coder/v2/cli/clitest"
2222
"github.com/coder/coder/v2/cli/cliui"
23+
"github.com/coder/coder/v2/coderd/healthcheck/health"
2324
"github.com/coder/coder/v2/coderd/util/ptr"
2425
"github.com/coder/coder/v2/codersdk"
26+
"github.com/coder/coder/v2/codersdk/healthsdk"
27+
"github.com/coder/coder/v2/codersdk/workspacesdk"
2528
"github.com/coder/coder/v2/tailnet"
2629
"github.com/coder/coder/v2/testutil"
2730
"github.com/coder/serpent"
@@ -672,3 +675,129 @@ func TestPeerDiagnostics(t *testing.T) {
672675
})
673676
}
674677
}
678+
679+
func TestConnDiagnostics(t *testing.T) {
680+
t.Parallel()
681+
testCases := []struct {
682+
name string
683+
diags cliui.ConnDiags
684+
want []string
685+
}{
686+
{
687+
name: "Direct",
688+
diags: cliui.ConnDiags{
689+
ConnInfo: &workspacesdk.AgentConnectionInfo{},
690+
PingP2P: true,
691+
LocalNetInfo: &tailcfg.NetInfo{},
692+
},
693+
want: []string{
694+
`✔ You are connected directly (p2p)`,
695+
},
696+
},
697+
{
698+
name: "DirectBlocked",
699+
diags: cliui.ConnDiags{
700+
ConnInfo: &workspacesdk.AgentConnectionInfo{
701+
DisableDirectConnections: true,
702+
},
703+
},
704+
want: []string{
705+
`❗ You are connected via a DERP relay, not directly (p2p)`,
706+
`❗ Your Coder administrator has blocked direct connections`,
707+
},
708+
},
709+
{
710+
name: "NoStun",
711+
diags: cliui.ConnDiags{
712+
ConnInfo: &workspacesdk.AgentConnectionInfo{
713+
DERPMap: &tailcfg.DERPMap{},
714+
},
715+
LocalNetInfo: &tailcfg.NetInfo{},
716+
},
717+
want: []string{
718+
`❗ You are connected via a DERP relay, not directly (p2p)`,
719+
`✘ The DERP map is not configured to use STUN, which will prevent direct connections from starting outside of local networks`,
720+
},
721+
},
722+
{
723+
name: "ClientHardNat",
724+
diags: cliui.ConnDiags{
725+
LocalNetInfo: &tailcfg.NetInfo{
726+
MappingVariesByDestIP: "true",
727+
},
728+
},
729+
want: []string{
730+
`❗ You are connected via a DERP relay, not directly (p2p)`,
731+
`❗ Client is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers`,
732+
},
733+
},
734+
{
735+
name: "AgentHardNat",
736+
diags: cliui.ConnDiags{
737+
ConnInfo: &workspacesdk.AgentConnectionInfo{},
738+
PingP2P: false,
739+
LocalNetInfo: &tailcfg.NetInfo{},
740+
AgentNetcheck: &healthsdk.AgentNetcheckReport{
741+
NetInfo: &tailcfg.NetInfo{MappingVariesByDestIP: "true"},
742+
},
743+
},
744+
want: []string{
745+
`❗ You are connected via a DERP relay, not directly (p2p)`,
746+
`❗ Agent is potentially behind a hard NAT, as multiple endpoints were retrieved from different STUN servers`,
747+
},
748+
},
749+
{
750+
name: "AgentInterfaceWarnings",
751+
diags: cliui.ConnDiags{
752+
PingP2P: true,
753+
AgentNetcheck: &healthsdk.AgentNetcheckReport{
754+
Interfaces: healthsdk.InterfacesReport{
755+
BaseReport: healthsdk.BaseReport{
756+
Warnings: []health.Message{
757+
health.Messagef(health.CodeInterfaceSmallMTU, "network interface eth0 has MTU 1280, (less than 1378), which may cause problems with direct connections"),
758+
},
759+
},
760+
},
761+
},
762+
},
763+
want: []string{
764+
`❗ Agent: network interface eth0 has MTU 1280, (less than 1378), which may cause problems with direct connections`,
765+
`✔ You are connected directly (p2p)`,
766+
},
767+
},
768+
{
769+
name: "LocalInterfaceWarnings",
770+
diags: cliui.ConnDiags{
771+
PingP2P: true,
772+
LocalInterfaces: &healthsdk.InterfacesReport{
773+
BaseReport: healthsdk.BaseReport{
774+
Warnings: []health.Message{
775+
health.Messagef(health.CodeInterfaceSmallMTU, "network interface eth1 has MTU 1310, (less than 1378), which may cause problems with direct connections"),
776+
},
777+
},
778+
},
779+
},
780+
want: []string{
781+
`❗ Client: network interface eth1 has MTU 1310, (less than 1378), which may cause problems with direct connections`,
782+
`✔ You are connected directly (p2p)`,
783+
},
784+
},
785+
}
786+
for _, tc := range testCases {
787+
tc := tc
788+
t.Run(tc.name, func(t *testing.T) {
789+
t.Parallel()
790+
r, w := io.Pipe()
791+
go func() {
792+
defer w.Close()
793+
cliui.ConnDiagnostics(w, tc.diags)
794+
}()
795+
bytes, err := io.ReadAll(r)
796+
require.NoError(t, err)
797+
output := string(bytes)
798+
for _, want := range tc.want {
799+
require.Contains(t, output, want)
800+
}
801+
})
802+
}
803+
}

cli/ping.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package cli
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"net/http"
68
"time"
79

810
"golang.org/x/xerrors"
@@ -14,6 +16,7 @@ import (
1416

1517
"github.com/coder/coder/v2/cli/cliui"
1618
"github.com/coder/coder/v2/codersdk"
19+
"github.com/coder/coder/v2/codersdk/healthsdk"
1720
"github.com/coder/coder/v2/codersdk/workspacesdk"
1821
"github.com/coder/serpent"
1922
)
@@ -61,7 +64,8 @@ func (r *RootCmd) ping() *serpent.Command {
6164
if !r.disableNetworkTelemetry {
6265
opts.EnableTelemetry = true
6366
}
64-
conn, err := workspacesdk.New(client).DialAgent(ctx, workspaceAgent.ID, opts)
67+
client := workspacesdk.New(client)
68+
conn, err := client.DialAgent(ctx, workspaceAgent.ID, opts)
6569
if err != nil {
6670
return err
6771
}
@@ -138,11 +142,44 @@ func (r *RootCmd) ping() *serpent.Command {
138142
)
139143

140144
if n == int(pingNum) {
141-
diags := conn.GetPeerDiagnostics()
142-
cliui.PeerDiagnostics(inv.Stdout, diags)
143-
return nil
145+
break
144146
}
145147
}
148+
ctx, cancel = context.WithTimeout(inv.Context(), 30*time.Second)
149+
defer cancel()
150+
diags := conn.GetPeerDiagnostics()
151+
cliui.PeerDiagnostics(inv.Stdout, diags)
152+
153+
connDiags := cliui.ConnDiags{
154+
PingP2P: didP2p,
155+
DisableDirect: r.disableDirect,
156+
LocalNetInfo: conn.GetNetInfo(),
157+
}
158+
connInfo, err := client.AgentConnectionInfoGeneric(ctx)
159+
if err == nil {
160+
connDiags.ConnInfo = &connInfo
161+
} else {
162+
_, _ = fmt.Fprintf(inv.Stdout, "Failed to retrieve connection info from server: %v\n", err)
163+
}
164+
ifReport, err := healthsdk.RunInterfacesReport()
165+
if err == nil {
166+
connDiags.LocalInterfaces = &ifReport
167+
} else {
168+
_, _ = fmt.Fprintf(inv.Stdout, "Failed to retrieve local interfaces report: %v\n", err)
169+
}
170+
agentNetcheck, err := conn.Netcheck(ctx)
171+
if err == nil {
172+
connDiags.AgentNetcheck = &agentNetcheck
173+
} else {
174+
var sdkErr *codersdk.Error
175+
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
176+
_, _ = fmt.Fprint(inv.Stdout, "Could not generate full connection report as the workspace agent is outdated\n")
177+
} else {
178+
_, _ = fmt.Fprintf(inv.Stdout, "Failed to retrieve connection report from agent: %v\n", err)
179+
}
180+
}
181+
cliui.ConnDiagnostics(inv.Stdout, connDiags)
182+
return nil
146183
},
147184
}
148185

codersdk/healthsdk/healthsdk.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,10 @@ type ClientNetcheckReport struct {
273273
DERP DERPHealthReport `json:"derp"`
274274
Interfaces InterfacesReport `json:"interfaces"`
275275
}
276+
277+
// @typescript-ignore AgentNetcheckReport
278+
type AgentNetcheckReport struct {
279+
BaseReport
280+
NetInfo *tailcfg.NetInfo `json:"net_info"`
281+
Interfaces InterfacesReport `json:"interfaces"`
282+
}

codersdk/workspacesdk/agentconn.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/coder/coder/v2/coderd/tracing"
2424
"github.com/coder/coder/v2/codersdk"
25+
"github.com/coder/coder/v2/codersdk/healthsdk"
2526
"github.com/coder/coder/v2/tailnet"
2627
)
2728

@@ -241,6 +242,23 @@ func (c *AgentConn) ListeningPorts(ctx context.Context) (codersdk.WorkspaceAgent
241242
return resp, json.NewDecoder(res.Body).Decode(&resp)
242243
}
243244

245+
// Netcheck returns a network check report from the workspace agent.
246+
func (c *AgentConn) Netcheck(ctx context.Context) (healthsdk.AgentNetcheckReport, error) {
247+
ctx, span := tracing.StartSpan(ctx)
248+
defer span.End()
249+
res, err := c.apiRequest(ctx, http.MethodGet, "/api/v0/netcheck", nil)
250+
if err != nil {
251+
return healthsdk.AgentNetcheckReport{}, xerrors.Errorf("do request: %w", err)
252+
}
253+
defer res.Body.Close()
254+
if res.StatusCode != http.StatusOK {
255+
return healthsdk.AgentNetcheckReport{}, codersdk.ReadBodyAsError(res)
256+
}
257+
258+
var resp healthsdk.AgentNetcheckReport
259+
return resp, json.NewDecoder(res.Body).Decode(&resp)
260+
}
261+
244262
// DebugMagicsock makes a request to the workspace agent's magicsock debug endpoint.
245263
func (c *AgentConn) DebugMagicsock(ctx context.Context) ([]byte, error) {
246264
ctx, span := tracing.StartSpan(ctx)

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