coder/coderd/healthcheck/derphealth/derp_test.go

485 lines
14 KiB
Go

package derphealth_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
)
//nolint:tparallel
func TestDERP(t *testing.T) {
if testing.Short() {
t.Skip("skipping healthcheck test in short mode, they reach out over the network.")
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
derpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
defer derpSrv.Close()
srv := httptest.NewServer(derphttp.Handler(derpSrv))
defer srv.Close()
var (
ctx = context.Background()
report = derphealth.Report{}
derpURL, _ = url.Parse(srv.URL)
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "1a",
RegionID: 999,
HostName: derpURL.Host,
IPv4: derpURL.Host,
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: true,
}},
},
}},
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
for _, region := range report.Regions {
assert.True(t, region.Healthy)
for _, node := range region.NodeReports {
assert.True(t, node.Healthy)
assert.True(t, node.CanExchangeMessages)
assert.Empty(t, node.Warnings)
assert.NotNil(t, node.Warnings)
assert.NotEmpty(t, node.RoundTripPing)
assert.Len(t, node.ClientLogs, 2)
assert.Len(t, node.ClientLogs[0], 3)
assert.Len(t, node.ClientErrs[0], 0)
assert.Len(t, node.ClientLogs[1], 3)
assert.Len(t, node.ClientErrs[1], 0)
assert.False(t, node.STUN.Enabled)
assert.False(t, node.STUN.CanSTUN)
assert.Nil(t, node.STUN.Error)
}
}
})
t.Run("HealthyWithNodeDegraded", func(t *testing.T) {
t.Parallel()
healthyDerpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
defer healthyDerpSrv.Close()
healthySrv := httptest.NewServer(derphttp.Handler(healthyDerpSrv))
defer healthySrv.Close()
var (
ctx = context.Background()
report = derphealth.Report{}
derpURL, _ = url.Parse(healthySrv.URL)
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "1a",
RegionID: 999,
HostName: derpURL.Host,
IPv4: derpURL.Host,
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: true,
}, {
Name: "1b",
RegionID: 999,
HostName: "derp.is.dead.tld",
IPv4: "derp.is.dead.tld",
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: true,
}},
},
}},
Dismissed: true, // Let's sneak an extra unit test
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
assert.True(t, report.Dismissed)
if assert.Len(t, report.Warnings, 1) {
assert.Contains(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.True(t, region.NodeReports[0].Healthy)
assert.Empty(t, region.NodeReports[0].Warnings)
assert.Equal(t, health.SeverityOK, region.NodeReports[0].Severity)
assert.False(t, region.NodeReports[1].Healthy)
assert.Equal(t, health.SeverityError, region.NodeReports[1].Severity)
assert.Len(t, region.Warnings, 1)
}
})
t.Run("HealthyWithNoSTUN", func(t *testing.T) {
t.Parallel()
healthyDerpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
defer healthyDerpSrv.Close()
healthySrv := httptest.NewServer(derphttp.Handler(healthyDerpSrv))
defer healthySrv.Close()
var (
ctx = context.Background()
report = derphealth.Report{}
derpURL, _ = url.Parse(healthySrv.URL)
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "1a",
RegionID: 999,
HostName: derpURL.Host,
IPv4: derpURL.Host,
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: true,
}, {
Name: "badstun",
RegionID: 999,
HostName: derpURL.Host,
STUNPort: 19302,
STUNOnly: true,
InsecureForTests: true,
ForceHTTP: true,
}},
},
}},
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
if assert.Len(t, report.Warnings, 2) {
assert.EqualValues(t, report.Warnings[1].Code, health.CodeSTUNNoNodes)
assert.EqualValues(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.True(t, region.NodeReports[0].Healthy)
assert.Empty(t, region.NodeReports[0].Warnings)
assert.NotNil(t, region.NodeReports[0].Warnings)
assert.Equal(t, health.SeverityOK, region.NodeReports[0].Severity)
assert.False(t, region.NodeReports[1].Healthy)
assert.Equal(t, health.SeverityError, region.NodeReports[1].Severity)
assert.Len(t, region.Warnings, 1)
}
})
t.Run("Tailscale/Dallas/OK", func(t *testing.T) {
t.Parallel()
if testutil.InCI() {
t.Skip("This test depends on reaching out over the network to Tailscale servers, which is inherently flaky.")
}
derpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
defer derpSrv.Close()
srv := httptest.NewServer(derphttp.Handler(derpSrv))
defer srv.Close()
var (
ctx = context.Background()
report = derphealth.Report{}
opts = &derphealth.ReportOptions{
DERPMap: tsDERPMap(ctx, t),
}
)
// Only include the Dallas region
opts.DERPMap.Regions = map[int]*tailcfg.DERPRegion{9: opts.DERPMap.Regions[9]}
report.Run(ctx, opts)
assert.True(t, report.Healthy)
for _, region := range report.Regions {
assert.True(t, region.Healthy)
for _, node := range region.NodeReports {
assert.True(t, node.Healthy)
assert.True(t, node.CanExchangeMessages)
assert.NotEmpty(t, node.RoundTripPing)
assert.Len(t, node.ClientLogs, 2)
// the exact number of logs depends on the certificates, which we don't control.
assert.GreaterOrEqual(t, len(node.ClientLogs[0]), 1)
assert.Len(t, node.ClientErrs[0], 0)
// the exact number of logs depends on the certificates, which we don't control.
assert.GreaterOrEqual(t, len(node.ClientLogs[1]), 1)
assert.Len(t, node.ClientErrs[1], 0)
assert.True(t, node.STUN.Enabled)
assert.True(t, node.STUN.CanSTUN)
assert.Nil(t, node.STUN.Error)
}
}
})
t.Run("FailoverToWebsockets", func(t *testing.T) {
t.Parallel()
derpSrv := derp.NewServer(key.NewNode(), func(format string, args ...any) { t.Logf(format, args...) })
defer derpSrv.Close()
handler, closeHandler := tailnet.WithWebsocketSupport(derpSrv, derphttp.Handler(derpSrv))
defer closeHandler()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") == "DERP" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("bad request"))
return
}
handler.ServeHTTP(w, r)
}))
var (
ctx = context.Background()
report = derphealth.Report{}
derpURL, _ = url.Parse(srv.URL)
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "1a",
RegionID: 999,
HostName: derpURL.Host,
IPv4: derpURL.Host,
STUNPort: -1,
InsecureForTests: true,
ForceHTTP: true,
}},
},
}},
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
if assert.NotEmpty(t, report.Warnings) {
assert.Equal(t, report.Warnings[0].Code, health.CodeDERPNodeUsesWebsocket)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityWarning, region.Severity)
assert.NotEmpty(t, region.Warnings)
for _, node := range region.NodeReports {
assert.True(t, node.Healthy)
assert.Equal(t, health.SeverityWarning, node.Severity)
assert.NotEmpty(t, node.Warnings)
assert.True(t, node.CanExchangeMessages)
assert.NotEmpty(t, node.RoundTripPing)
assert.Len(t, node.ClientLogs, 2)
assert.Len(t, node.ClientLogs[0], 5)
assert.Len(t, node.ClientLogs[1], 5)
assert.Len(t, node.ClientErrs, 2)
assert.Len(t, node.ClientErrs[0], 1) // this
assert.Len(t, node.ClientErrs[1], 1)
assert.True(t, node.UsesWebsocket)
assert.False(t, node.STUN.Enabled)
assert.False(t, node.STUN.CanSTUN)
assert.Nil(t, node.STUN.Error)
}
}
})
t.Run("STUNOnly/OK", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
report = derphealth.Report{}
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "999stun0",
RegionID: 999,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
InsecureForTests: true,
ForceHTTP: true,
}},
},
}},
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityOK, report.Severity)
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityOK, region.Severity)
for _, node := range region.NodeReports {
assert.True(t, node.Healthy)
assert.False(t, node.CanExchangeMessages)
assert.Len(t, node.ClientLogs, 0)
assert.True(t, node.STUN.Enabled)
assert.True(t, node.STUN.CanSTUN)
assert.Nil(t, node.STUN.Error)
}
}
})
t.Run("STUNOnly/OneBadOneGood", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
report = derphealth.Report{}
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "badstun",
RegionID: 999,
HostName: "badstun.example.com",
STUNPort: 19302,
STUNOnly: true,
InsecureForTests: true,
ForceHTTP: true,
}, {
Name: "goodstun",
RegionID: 999,
HostName: "stun.l.google.com",
STUNPort: 19302,
STUNOnly: true,
InsecureForTests: true,
ForceHTTP: true,
}},
},
},
},
}
)
report.Run(ctx, opts)
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
if assert.Len(t, report.Warnings, 1) {
assert.Equal(t, health.CodeDERPOneNodeUnhealthy, report.Warnings[0].Code)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
assert.Equal(t, health.SeverityWarning, region.Severity)
// badstun
assert.False(t, region.NodeReports[0].Healthy)
assert.True(t, region.NodeReports[0].STUN.Enabled)
assert.False(t, region.NodeReports[0].STUN.CanSTUN)
assert.NotNil(t, region.NodeReports[0].STUN.Error)
// goodstun
assert.True(t, region.NodeReports[1].Healthy)
assert.True(t, region.NodeReports[1].STUN.Enabled)
assert.True(t, region.NodeReports[1].STUN.CanSTUN)
assert.Nil(t, region.NodeReports[1].STUN.Error)
}
})
t.Run("STUNOnly/NoStun", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
report = derphealth.Report{}
opts = &derphealth.ReportOptions{
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
EmbeddedRelay: true,
RegionID: 999,
Nodes: []*tailcfg.DERPNode{{
Name: "badstun",
RegionID: 999,
HostName: "badstun.example.com",
STUNPort: 19302,
STUNOnly: true,
InsecureForTests: true,
ForceHTTP: true,
}},
},
},
},
}
)
report.Run(ctx, opts)
assert.False(t, report.Healthy)
assert.Equal(t, health.SeverityError, report.Severity)
for _, region := range report.Regions {
assert.False(t, region.Healthy)
assert.Equal(t, health.SeverityError, region.Severity)
for _, node := range region.NodeReports {
assert.False(t, node.Healthy)
assert.False(t, node.CanExchangeMessages)
assert.Empty(t, node.ClientLogs)
assert.True(t, node.STUN.Enabled)
assert.False(t, node.STUN.CanSTUN)
assert.NotNil(t, node.STUN.Error)
}
}
})
}
func tsDERPMap(ctx context.Context, t testing.TB) *tailcfg.DERPMap {
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
var derpMap tailcfg.DERPMap
err = json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&derpMap)
require.NoError(t, err)
return &derpMap
}