mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): add DERP healthcheck (#6936)
This commit is contained in:
Generated
+222
@@ -359,6 +359,31 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/debug/health": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Debug"
|
||||
],
|
||||
"summary": "Debug Info Deployment Health",
|
||||
"operationId": "debug-info-deployment-health",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/healthcheck.Report"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/deployment/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -9487,6 +9512,203 @@ const docTemplate = `{
|
||||
"ParameterSourceSchemeData"
|
||||
]
|
||||
},
|
||||
"healthcheck.DERPNodeReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_exchange_messages": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"client_errs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"client_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"node": {
|
||||
"$ref": "#/definitions/tailcfg.DERPNode"
|
||||
},
|
||||
"round_trip_ping": {
|
||||
"type": "integer"
|
||||
},
|
||||
"stun": {
|
||||
"$ref": "#/definitions/healthcheck.DERPStunReport"
|
||||
},
|
||||
"uses_websocket": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPRegionReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"node_reports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/healthcheck.DERPNodeReport"
|
||||
}
|
||||
},
|
||||
"region": {
|
||||
"$ref": "#/definitions/tailcfg.DERPRegion"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"netcheck": {
|
||||
"$ref": "#/definitions/netcheck.Report"
|
||||
},
|
||||
"netcheck_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"regions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/healthcheck.DERPRegionReport"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPStunReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canSTUN": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"healthcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"derp": {
|
||||
"$ref": "#/definitions/healthcheck.DERPReport"
|
||||
},
|
||||
"pass": {
|
||||
"description": "Healthy is true if the report returns no errors.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time": {
|
||||
"description": "Time is the time the report was generated at.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"captivePortal": {
|
||||
"description": "CaptivePortal is set when we think there's a captive portal that is\nintercepting HTTP traffic.",
|
||||
"type": "string"
|
||||
},
|
||||
"globalV4": {
|
||||
"description": "ip:port of global IPv4",
|
||||
"type": "string"
|
||||
},
|
||||
"globalV6": {
|
||||
"description": "[ip]:port of global IPv6",
|
||||
"type": "string"
|
||||
},
|
||||
"hairPinning": {
|
||||
"description": "HairPinning is whether the router supports communicating\nbetween two local devices through the NATted public IP address\n(on IPv4).",
|
||||
"type": "string"
|
||||
},
|
||||
"icmpv4": {
|
||||
"description": "an ICMPv4 round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv4": {
|
||||
"description": "an IPv4 STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv4CanSend": {
|
||||
"description": "an IPv4 packet was able to be sent",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6": {
|
||||
"description": "an IPv6 STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6CanSend": {
|
||||
"description": "an IPv6 packet was able to be sent",
|
||||
"type": "boolean"
|
||||
},
|
||||
"mappingVariesByDestIP": {
|
||||
"description": "MappingVariesByDestIP is whether STUN results depend which\nSTUN server you're talking to (on IPv4).",
|
||||
"type": "string"
|
||||
},
|
||||
"oshasIPv6": {
|
||||
"description": "could bind a socket to ::1",
|
||||
"type": "boolean"
|
||||
},
|
||||
"pcp": {
|
||||
"description": "PCP is whether PCP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
},
|
||||
"pmp": {
|
||||
"description": "PMP is whether NAT-PMP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
},
|
||||
"preferredDERP": {
|
||||
"description": "or 0 for unknown",
|
||||
"type": "integer"
|
||||
},
|
||||
"regionLatency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"udp": {
|
||||
"description": "a UDP STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"upnP": {
|
||||
"description": "UPnP is whether UPnP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameter.ComputedValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+218
@@ -305,6 +305,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/debug/health": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Debug"],
|
||||
"summary": "Debug Info Deployment Health",
|
||||
"operationId": "debug-info-deployment-health",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/healthcheck.Report"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/deployment/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -8564,6 +8585,203 @@
|
||||
"ParameterSourceSchemeData"
|
||||
]
|
||||
},
|
||||
"healthcheck.DERPNodeReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_exchange_messages": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"client_errs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"client_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"node": {
|
||||
"$ref": "#/definitions/tailcfg.DERPNode"
|
||||
},
|
||||
"round_trip_ping": {
|
||||
"type": "integer"
|
||||
},
|
||||
"stun": {
|
||||
"$ref": "#/definitions/healthcheck.DERPStunReport"
|
||||
},
|
||||
"uses_websocket": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPRegionReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"node_reports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/healthcheck.DERPNodeReport"
|
||||
}
|
||||
},
|
||||
"region": {
|
||||
"$ref": "#/definitions/tailcfg.DERPRegion"
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"netcheck": {
|
||||
"$ref": "#/definitions/netcheck.Report"
|
||||
},
|
||||
"netcheck_logs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"regions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/healthcheck.DERPRegionReport"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthcheck.DERPStunReport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canSTUN": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"healthcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"derp": {
|
||||
"$ref": "#/definitions/healthcheck.DERPReport"
|
||||
},
|
||||
"pass": {
|
||||
"description": "Healthy is true if the report returns no errors.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time": {
|
||||
"description": "Time is the time the report was generated at.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netcheck.Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"captivePortal": {
|
||||
"description": "CaptivePortal is set when we think there's a captive portal that is\nintercepting HTTP traffic.",
|
||||
"type": "string"
|
||||
},
|
||||
"globalV4": {
|
||||
"description": "ip:port of global IPv4",
|
||||
"type": "string"
|
||||
},
|
||||
"globalV6": {
|
||||
"description": "[ip]:port of global IPv6",
|
||||
"type": "string"
|
||||
},
|
||||
"hairPinning": {
|
||||
"description": "HairPinning is whether the router supports communicating\nbetween two local devices through the NATted public IP address\n(on IPv4).",
|
||||
"type": "string"
|
||||
},
|
||||
"icmpv4": {
|
||||
"description": "an ICMPv4 round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv4": {
|
||||
"description": "an IPv4 STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv4CanSend": {
|
||||
"description": "an IPv4 packet was able to be sent",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6": {
|
||||
"description": "an IPv6 STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6CanSend": {
|
||||
"description": "an IPv6 packet was able to be sent",
|
||||
"type": "boolean"
|
||||
},
|
||||
"mappingVariesByDestIP": {
|
||||
"description": "MappingVariesByDestIP is whether STUN results depend which\nSTUN server you're talking to (on IPv4).",
|
||||
"type": "string"
|
||||
},
|
||||
"oshasIPv6": {
|
||||
"description": "could bind a socket to ::1",
|
||||
"type": "boolean"
|
||||
},
|
||||
"pcp": {
|
||||
"description": "PCP is whether PCP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
},
|
||||
"pmp": {
|
||||
"description": "PMP is whether NAT-PMP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
},
|
||||
"preferredDERP": {
|
||||
"description": "or 0 for unknown",
|
||||
"type": "integer"
|
||||
},
|
||||
"regionLatency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"description": "keyed by DERP Region ID",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"udp": {
|
||||
"description": "a UDP STUN round trip completed",
|
||||
"type": "boolean"
|
||||
},
|
||||
"upnP": {
|
||||
"description": "UPnP is whether UPnP appears present on the LAN.\nEmpty means not checked.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameter.ComputedValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+23
-1
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/singleflight"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/buildinfo"
|
||||
@@ -46,6 +47,7 @@ import (
|
||||
"github.com/coder/coder/coderd/database/dbtype"
|
||||
"github.com/coder/coder/coderd/gitauth"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/metricscache"
|
||||
@@ -123,7 +125,10 @@ type Options struct {
|
||||
TemplateScheduleStore schedule.TemplateScheduleStore
|
||||
// AppSigningKey denotes the symmetric key to use for signing app tickets.
|
||||
// The key must be 64 bytes long.
|
||||
AppSigningKey []byte
|
||||
AppSigningKey []byte
|
||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
||||
HealthcheckTimeout time.Duration
|
||||
HealthcheckRefresh time.Duration
|
||||
|
||||
// APIRateLimit is the minutely throughput rate limit per user or ip.
|
||||
// Setting a rate limit <0 will disable the rate limiter across the entire
|
||||
@@ -235,6 +240,19 @@ func New(options *Options) *API {
|
||||
if len(options.AppSigningKey) != 64 {
|
||||
panic("coderd: AppSigningKey must be 64 bytes long")
|
||||
}
|
||||
if options.HealthcheckFunc == nil {
|
||||
options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) {
|
||||
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
|
||||
DERPMap: options.DERPMap.Clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if options.HealthcheckTimeout == 0 {
|
||||
options.HealthcheckTimeout = 30 * time.Second
|
||||
}
|
||||
if options.HealthcheckRefresh == 0 {
|
||||
options.HealthcheckRefresh = 10 * time.Minute
|
||||
}
|
||||
|
||||
siteCacheDir := options.CacheDir
|
||||
if siteCacheDir != "" {
|
||||
@@ -293,6 +311,7 @@ func New(options *Options) *API {
|
||||
Auditor: atomic.Pointer[audit.Auditor]{},
|
||||
TemplateScheduleStore: atomic.Pointer[schedule.TemplateScheduleStore]{},
|
||||
Experiments: experiments,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthcheck.Report]{},
|
||||
}
|
||||
if options.UpdateCheckOptions != nil {
|
||||
api.updateChecker = updatecheck.New(
|
||||
@@ -718,6 +737,7 @@ func New(options *Options) *API {
|
||||
)
|
||||
|
||||
r.Get("/coordinator", api.debugCoordinator)
|
||||
r.Get("/health", api.debugDeploymentHealth)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -773,6 +793,8 @@ type API struct {
|
||||
// Experiments contains the list of experiments currently enabled.
|
||||
// This is used to gate features that are not yet ready for production.
|
||||
Experiments codersdk.Experiments
|
||||
|
||||
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
|
||||
}
|
||||
|
||||
// Close waits for all WebSocket connections to drain before returning.
|
||||
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
"github.com/coder/coder/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/coderd/gitauth"
|
||||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
@@ -105,6 +106,10 @@ type Options struct {
|
||||
TrialGenerator func(context.Context, string) error
|
||||
TemplateScheduleStore schedule.TemplateScheduleStore
|
||||
|
||||
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
|
||||
HealthcheckTimeout time.Duration
|
||||
HealthcheckRefresh time.Duration
|
||||
|
||||
// All rate limits default to -1 (unlimited) in tests if not set.
|
||||
APIRateLimit int
|
||||
LoginRateLimit int
|
||||
@@ -335,6 +340,9 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
|
||||
SwaggerEndpoint: options.SwaggerEndpoint,
|
||||
AppSigningKey: AppSigningKey,
|
||||
SSHConfig: options.ConfigSSH,
|
||||
HealthcheckFunc: options.HealthcheckFunc,
|
||||
HealthcheckTimeout: options.HealthcheckTimeout,
|
||||
HealthcheckRefresh: options.HealthcheckRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-1
@@ -1,6 +1,14 @@
|
||||
package coderd
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
// @Summary Debug Info Wireguard Coordinator
|
||||
// @ID debug-info-wireguard-coordinator
|
||||
@@ -12,3 +20,36 @@ import "net/http"
|
||||
func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) {
|
||||
(*api.TailnetCoordinator.Load()).ServeHTTPDebug(rw, r)
|
||||
}
|
||||
|
||||
// @Summary Debug Info Deployment Health
|
||||
// @ID debug-info-deployment-health
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Debug
|
||||
// @Success 200 {object} healthcheck.Report
|
||||
// @Router /debug/health [get]
|
||||
func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout)
|
||||
defer cancel()
|
||||
|
||||
resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) {
|
||||
return api.HealthcheckFunc(ctx)
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Healthcheck is in progress and did not complete in time. Try again in a few seconds.",
|
||||
})
|
||||
return
|
||||
case res := <-resChan:
|
||||
if time.Since(res.Val.Time) > api.HealthcheckRefresh {
|
||||
api.healthCheckGroup.Forget("")
|
||||
api.debugDeploymentHealth(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, res.Val)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/healthcheck"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Health/OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
||||
return &healthcheck.Report{}, nil
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.Request(ctx, "GET", "/debug/health", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
_, _ = io.ReadAll(res.Body)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Health/Timeout", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
HealthcheckTimeout: time.Microsecond,
|
||||
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
|
||||
t := time.NewTimer(time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-t.C:
|
||||
return &healthcheck.Report{}, nil
|
||||
}
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
_, _ = io.ReadAll(res.Body)
|
||||
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/prober"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type DERPReport struct {
|
||||
mu sync.Mutex
|
||||
Healthy bool `json:"healthy"`
|
||||
|
||||
Regions map[int]*DERPRegionReport `json:"regions"`
|
||||
|
||||
Netcheck *netcheck.Report `json:"netcheck"`
|
||||
NetcheckLogs []string `json:"netcheck_logs"`
|
||||
}
|
||||
|
||||
type DERPRegionReport struct {
|
||||
mu sync.Mutex
|
||||
Healthy bool `json:"healthy"`
|
||||
|
||||
Region *tailcfg.DERPRegion `json:"region"`
|
||||
NodeReports []*DERPNodeReport `json:"node_reports"`
|
||||
}
|
||||
type DERPNodeReport struct {
|
||||
mu sync.Mutex
|
||||
clientCounter int
|
||||
|
||||
Healthy bool `json:"healthy"`
|
||||
Node *tailcfg.DERPNode `json:"node"`
|
||||
|
||||
CanExchangeMessages bool `json:"can_exchange_messages"`
|
||||
RoundTripPing time.Duration `json:"round_trip_ping"`
|
||||
UsesWebsocket bool `json:"uses_websocket"`
|
||||
ClientLogs [][]string `json:"client_logs"`
|
||||
ClientErrs [][]error `json:"client_errs"`
|
||||
|
||||
STUN DERPStunReport `json:"stun"`
|
||||
}
|
||||
|
||||
type DERPStunReport struct {
|
||||
Enabled bool
|
||||
CanSTUN bool
|
||||
Error error
|
||||
}
|
||||
|
||||
type DERPReportOptions struct {
|
||||
DERPMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (r *DERPReport) Run(ctx context.Context, opts *DERPReportOptions) error {
|
||||
r.Healthy = true
|
||||
r.Regions = map[int]*DERPRegionReport{}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, region := range opts.DERPMap.Regions {
|
||||
region := region
|
||||
eg.Go(func() error {
|
||||
regionReport := DERPRegionReport{
|
||||
Region: region,
|
||||
}
|
||||
|
||||
err := regionReport.Run(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run region report: %w", err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.Regions[region.RegionID] = ®ionReport
|
||||
if !regionReport.Healthy {
|
||||
r.Healthy = false
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
ncLogf := func(format string, args ...interface{}) {
|
||||
r.mu.Lock()
|
||||
r.NetcheckLogs = append(r.NetcheckLogs, fmt.Sprintf(format, args...))
|
||||
r.mu.Unlock()
|
||||
}
|
||||
nc := &netcheck.Client{
|
||||
PortMapper: portmapper.NewClient(tslogger.WithPrefix(ncLogf, "portmap: "), nil),
|
||||
Logf: tslogger.WithPrefix(ncLogf, "netcheck: "),
|
||||
}
|
||||
ncReport, err := nc.GetReport(ctx, opts.DERPMap)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run netcheck: %w", err)
|
||||
}
|
||||
r.Netcheck = ncReport
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (r *DERPRegionReport) Run(ctx context.Context) error {
|
||||
r.Healthy = true
|
||||
r.NodeReports = []*DERPNodeReport{}
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, node := range r.Region.Nodes {
|
||||
node := node
|
||||
eg.Go(func() error {
|
||||
nodeReport := DERPNodeReport{
|
||||
Node: node,
|
||||
Healthy: true,
|
||||
}
|
||||
|
||||
err := nodeReport.Run(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run node report: %w", err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.NodeReports = append(r.NodeReports, &nodeReport)
|
||||
if !nodeReport.Healthy {
|
||||
r.Healthy = false
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) derpURL() *url.URL {
|
||||
derpURL := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: r.Node.HostName,
|
||||
Path: "/derp",
|
||||
}
|
||||
if r.Node.ForceHTTP {
|
||||
derpURL.Scheme = "http"
|
||||
}
|
||||
if r.Node.HostName == "" {
|
||||
derpURL.Host = fmt.Sprintf("%s:%d", r.Node.IPv4, r.Node.DERPPort)
|
||||
}
|
||||
|
||||
return derpURL
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r.ClientLogs = [][]string{}
|
||||
r.ClientErrs = [][]error{}
|
||||
|
||||
r.doExchangeMessage(ctx)
|
||||
r.doSTUNTest(ctx)
|
||||
|
||||
if !r.CanExchangeMessages || r.UsesWebsocket || r.STUN.Error != nil {
|
||||
r.Healthy = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) doExchangeMessage(ctx context.Context) {
|
||||
if r.Node.STUNOnly {
|
||||
return
|
||||
}
|
||||
|
||||
var peerKey atomic.Pointer[key.NodePublic]
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
receive, receiveID, err := r.derpClient(ctx, r.derpURL())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer receive.Close()
|
||||
|
||||
eg.Go(func() error {
|
||||
defer receive.Close()
|
||||
|
||||
pkt, err := r.recvData(receive)
|
||||
if err != nil {
|
||||
r.writeClientErr(receiveID, xerrors.Errorf("recv derp message: %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
if *peerKey.Load() != pkt.Source {
|
||||
r.writeClientErr(receiveID, xerrors.Errorf("received pkt from unknown peer: %s", pkt.Source.ShortString()))
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := time.Parse(time.RFC3339Nano, string(pkt.Data))
|
||||
if err != nil {
|
||||
r.writeClientErr(receiveID, xerrors.Errorf("parse time from peer: %w", err))
|
||||
return err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.CanExchangeMessages = true
|
||||
r.RoundTripPing = time.Since(t)
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
eg.Go(func() error {
|
||||
send, sendID, err := r.derpClient(ctx, r.derpURL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer send.Close()
|
||||
|
||||
key := send.SelfPublicKey()
|
||||
peerKey.Store(&key)
|
||||
|
||||
err = send.Send(receive.SelfPublicKey(), []byte(time.Now().Format(time.RFC3339Nano)))
|
||||
if err != nil {
|
||||
r.writeClientErr(sendID, xerrors.Errorf("send derp message: %w", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = eg.Wait()
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) doSTUNTest(ctx context.Context) {
|
||||
if r.Node.STUNPort == -1 {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.STUN.Enabled = true
|
||||
r.mu.Unlock()
|
||||
|
||||
addr, port, err := r.stunAddr(ctx)
|
||||
if err != nil {
|
||||
r.STUN.Error = xerrors.Errorf("get stun addr: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// We only create a prober to call ProbeUDP manually.
|
||||
p, err := prober.DERP(prober.New(), "", time.Second, time.Second, time.Second)
|
||||
if err != nil {
|
||||
r.STUN.Error = xerrors.Errorf("create prober: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.ProbeUDP(addr, port)(ctx)
|
||||
if err != nil {
|
||||
r.STUN.Error = xerrors.Errorf("probe stun: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.STUN.CanSTUN = true
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) stunAddr(ctx context.Context) (string, int, error) {
|
||||
port := r.Node.STUNPort
|
||||
if port == 0 {
|
||||
port = 3478
|
||||
}
|
||||
if port < 0 || port > 1<<16-1 {
|
||||
return "", 0, xerrors.Errorf("invalid stun port %d", port)
|
||||
}
|
||||
|
||||
if r.Node.STUNTestIP != "" {
|
||||
ip, err := netip.ParseAddr(r.Node.STUNTestIP)
|
||||
if err != nil {
|
||||
return "", 0, xerrors.Errorf("invalid stun test ip %q: %w", r.Node.STUNTestIP, err)
|
||||
}
|
||||
|
||||
return ip.String(), port, nil
|
||||
}
|
||||
|
||||
if r.Node.HostName != "" {
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, r.Node.HostName)
|
||||
if err != nil {
|
||||
return "", 0, xerrors.Errorf("lookup ip addr: %w", err)
|
||||
}
|
||||
for _, a := range addrs {
|
||||
return a.String(), port, nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.Node.IPv4 != "" {
|
||||
ip, err := netip.ParseAddr(r.Node.IPv4)
|
||||
if err != nil {
|
||||
return "", 0, xerrors.Errorf("invalid ipv4 %q: %w", r.Node.IPv4, err)
|
||||
}
|
||||
|
||||
if !ip.Is4() {
|
||||
return "", 0, xerrors.Errorf("provided node ipv4 is not v4 %q: %w", r.Node.IPv4, err)
|
||||
}
|
||||
|
||||
return ip.String(), port, nil
|
||||
}
|
||||
if r.Node.IPv6 != "" {
|
||||
ip, err := netip.ParseAddr(r.Node.IPv6)
|
||||
if err != nil {
|
||||
return "", 0, xerrors.Errorf("invalid ipv6 %q: %w", r.Node.IPv6, err)
|
||||
}
|
||||
|
||||
if !ip.Is6() {
|
||||
return "", 0, xerrors.Errorf("provided node ipv6 is not v6 %q: %w", r.Node.IPv6, err)
|
||||
}
|
||||
|
||||
return ip.String(), port, nil
|
||||
}
|
||||
|
||||
return "", 0, xerrors.New("no stun ips provided")
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) writeClientErr(clientID int, err error) {
|
||||
r.mu.Lock()
|
||||
r.ClientErrs[clientID] = append(r.ClientErrs[clientID], err)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *DERPNodeReport) derpClient(ctx context.Context, derpURL *url.URL) (*derphttp.Client, int, error) {
|
||||
r.mu.Lock()
|
||||
id := r.clientCounter
|
||||
r.clientCounter++
|
||||
r.ClientLogs = append(r.ClientLogs, []string{})
|
||||
r.ClientErrs = append(r.ClientErrs, []error{})
|
||||
r.mu.Unlock()
|
||||
|
||||
client, err := derphttp.NewClient(key.NewNode(), derpURL.String(), func(format string, args ...any) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
if strings.Contains(msg, "We'll use WebSockets on the next connection attempt") {
|
||||
r.UsesWebsocket = true
|
||||
}
|
||||
r.ClientLogs[id] = append(r.ClientLogs[id], msg)
|
||||
})
|
||||
if err != nil {
|
||||
err := xerrors.Errorf("create derp client: %w", err)
|
||||
r.writeClientErr(id, err)
|
||||
return nil, id, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = client.Close()
|
||||
}()
|
||||
|
||||
i := 0
|
||||
for ; i < 5; i++ {
|
||||
err = client.Connect(ctx)
|
||||
if err != nil {
|
||||
r.writeClientErr(id, xerrors.Errorf("connect to derp: %w", err))
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i == 5 {
|
||||
err := xerrors.Errorf("couldn't connect after 5 tries, last error: %w", err)
|
||||
r.writeClientErr(id, xerrors.Errorf("couldn't connect after 5 tries, last error: %w", err))
|
||||
return nil, id, err
|
||||
}
|
||||
|
||||
return client, id, nil
|
||||
}
|
||||
|
||||
func (*DERPNodeReport) recvData(client *derphttp.Client) (derp.ReceivedPacket, error) {
|
||||
for {
|
||||
msg, err := client.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return derp.ReceivedPacket{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case derp.ReceivedPacket:
|
||||
return msg, nil
|
||||
default:
|
||||
// Drop all others!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package healthcheck_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/coderd/healthcheck"
|
||||
"github.com/coder/coder/tailnet"
|
||||
)
|
||||
|
||||
func TestDERP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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 = healthcheck.DERPReport{}
|
||||
derpURL, _ = url.Parse(srv.URL)
|
||||
opts = &healthcheck.DERPReportOptions{
|
||||
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,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
)
|
||||
|
||||
err := report.Run(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.Positive(t, node.RoundTripPing)
|
||||
assert.Len(t, node.ClientLogs, 2)
|
||||
assert.Len(t, node.ClientLogs[0], 1)
|
||||
assert.Len(t, node.ClientErrs[0], 0)
|
||||
assert.Len(t, node.ClientLogs[1], 1)
|
||||
assert.Len(t, node.ClientErrs[1], 0)
|
||||
|
||||
assert.False(t, node.STUN.Enabled)
|
||||
assert.False(t, node.STUN.CanSTUN)
|
||||
assert.NoError(t, node.STUN.Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OK/Tailscale/Dallas", 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 = healthcheck.DERPReport{}
|
||||
opts = &healthcheck.DERPReportOptions{
|
||||
DERPMap: tsDERPMap(ctx, t),
|
||||
}
|
||||
)
|
||||
// Only include the Dallas region
|
||||
opts.DERPMap.Regions = map[int]*tailcfg.DERPRegion{9: opts.DERPMap.Regions[9]}
|
||||
|
||||
err := report.Run(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.Positive(t, node.RoundTripPing)
|
||||
assert.Len(t, node.ClientLogs, 2)
|
||||
assert.Len(t, node.ClientLogs[0], 1)
|
||||
assert.Len(t, node.ClientErrs[0], 0)
|
||||
assert.Len(t, node.ClientLogs[1], 1)
|
||||
assert.Len(t, node.ClientErrs[1], 0)
|
||||
|
||||
assert.True(t, node.STUN.Enabled)
|
||||
assert.True(t, node.STUN.CanSTUN)
|
||||
assert.NoError(t, node.STUN.Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ForceWebsockets", 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 = healthcheck.DERPReport{}
|
||||
derpURL, _ = url.Parse(srv.URL)
|
||||
opts = &healthcheck.DERPReportOptions{
|
||||
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.False(t, report.Healthy)
|
||||
for _, region := range report.Regions {
|
||||
assert.False(t, region.Healthy)
|
||||
for _, node := range region.NodeReports {
|
||||
assert.False(t, node.Healthy)
|
||||
assert.True(t, node.CanExchangeMessages)
|
||||
assert.Positive(t, node.RoundTripPing)
|
||||
assert.Len(t, node.ClientLogs, 2)
|
||||
assert.Len(t, node.ClientLogs[0], 3)
|
||||
assert.Len(t, node.ClientLogs[1], 3)
|
||||
assert.Len(t, node.ClientErrs, 2)
|
||||
assert.Len(t, node.ClientErrs[0], 1)
|
||||
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.NoError(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
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
// Time is the time the report was generated at.
|
||||
Time time.Time `json:"time"`
|
||||
// Healthy is true if the report returns no errors.
|
||||
Healthy bool `json:"pass"`
|
||||
|
||||
DERP DERPReport `json:"derp"`
|
||||
|
||||
// TODO
|
||||
// AccessURL AccessURLReport
|
||||
// Websocket WebsocketReport
|
||||
}
|
||||
|
||||
type ReportOptions struct {
|
||||
// TODO: support getting this over HTTP?
|
||||
DERPMap *tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
|
||||
var report Report
|
||||
|
||||
err := report.DERP.Run(ctx, &DERPReportOptions{
|
||||
DERPMap: opts.DERPMap,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("run derp: %w", err)
|
||||
}
|
||||
|
||||
report.Time = time.Now()
|
||||
report.Healthy = report.DERP.Healthy
|
||||
return &report, nil
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package healthcheck
|
||||
|
||||
type WebsocketReport struct{}
|
||||
@@ -19,3 +19,181 @@ curl -X GET http://coder-server:8080/api/v2/debug/coordinator \
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Debug Info Deployment Health
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/debug/health \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /debug/health`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"derp": {
|
||||
"healthy": true,
|
||||
"netcheck": {
|
||||
"captivePortal": "string",
|
||||
"globalV4": "string",
|
||||
"globalV6": "string",
|
||||
"hairPinning": "string",
|
||||
"icmpv4": true,
|
||||
"ipv4": true,
|
||||
"ipv4CanSend": true,
|
||||
"ipv6": true,
|
||||
"ipv6CanSend": true,
|
||||
"mappingVariesByDestIP": "string",
|
||||
"oshasIPv6": true,
|
||||
"pcp": "string",
|
||||
"pmp": "string",
|
||||
"preferredDERP": 0,
|
||||
"regionLatency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
},
|
||||
"property2": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"time": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [healthcheck.Report](schemas.md#healthcheckreport) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -5456,6 +5456,523 @@ Parameter represents a set value for the scope.
|
||||
| `none` |
|
||||
| `data` |
|
||||
|
||||
## healthcheck.DERPNodeReport
|
||||
|
||||
```json
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------------- | -------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `can_exchange_messages` | boolean | false | | |
|
||||
| `client_errs` | array of array | false | | |
|
||||
| `client_logs` | array of array | false | | |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `node` | [tailcfg.DERPNode](#tailcfgderpnode) | false | | |
|
||||
| `round_trip_ping` | integer | false | | |
|
||||
| `stun` | [healthcheck.DERPStunReport](#healthcheckderpstunreport) | false | | |
|
||||
| `uses_websocket` | boolean | false | | |
|
||||
|
||||
## healthcheck.DERPRegionReport
|
||||
|
||||
```json
|
||||
{
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `node_reports` | array of [healthcheck.DERPNodeReport](#healthcheckderpnodereport) | false | | |
|
||||
| `region` | [tailcfg.DERPRegion](#tailcfgderpregion) | false | | |
|
||||
|
||||
## healthcheck.DERPReport
|
||||
|
||||
```json
|
||||
{
|
||||
"healthy": true,
|
||||
"netcheck": {
|
||||
"captivePortal": "string",
|
||||
"globalV4": "string",
|
||||
"globalV6": "string",
|
||||
"hairPinning": "string",
|
||||
"icmpv4": true,
|
||||
"ipv4": true,
|
||||
"ipv4CanSend": true,
|
||||
"ipv6": true,
|
||||
"ipv6CanSend": true,
|
||||
"mappingVariesByDestIP": "string",
|
||||
"oshasIPv6": true,
|
||||
"pcp": "string",
|
||||
"pmp": "string",
|
||||
"preferredDERP": 0,
|
||||
"regionLatency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
},
|
||||
"property2": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------ | ------------------------------------------------------------ | -------- | ------------ | ----------- |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `netcheck` | [netcheck.Report](#netcheckreport) | false | | |
|
||||
| `netcheck_logs` | array of string | false | | |
|
||||
| `regions` | object | false | | |
|
||||
| » `[any property]` | [healthcheck.DERPRegionReport](#healthcheckderpregionreport) | false | | |
|
||||
|
||||
## healthcheck.DERPStunReport
|
||||
|
||||
```json
|
||||
{
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| --------- | ------- | -------- | ------------ | ----------- |
|
||||
| `canSTUN` | boolean | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `error` | any | false | | |
|
||||
|
||||
## healthcheck.Report
|
||||
|
||||
```json
|
||||
{
|
||||
"derp": {
|
||||
"healthy": true,
|
||||
"netcheck": {
|
||||
"captivePortal": "string",
|
||||
"globalV4": "string",
|
||||
"globalV6": "string",
|
||||
"hairPinning": "string",
|
||||
"icmpv4": true,
|
||||
"ipv4": true,
|
||||
"ipv4CanSend": true,
|
||||
"ipv6": true,
|
||||
"ipv6CanSend": true,
|
||||
"mappingVariesByDestIP": "string",
|
||||
"oshasIPv6": true,
|
||||
"pcp": "string",
|
||||
"pmp": "string",
|
||||
"preferredDERP": 0,
|
||||
"regionLatency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
},
|
||||
"netcheck_logs": ["string"],
|
||||
"regions": {
|
||||
"property1": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
},
|
||||
"property2": {
|
||||
"healthy": true,
|
||||
"node_reports": [
|
||||
{
|
||||
"can_exchange_messages": true,
|
||||
"client_errs": [[null]],
|
||||
"client_logs": [["string"]],
|
||||
"healthy": true,
|
||||
"node": {
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
},
|
||||
"round_trip_ping": 0,
|
||||
"stun": {
|
||||
"canSTUN": true,
|
||||
"enabled": true,
|
||||
"error": null
|
||||
},
|
||||
"uses_websocket": true
|
||||
}
|
||||
],
|
||||
"region": {
|
||||
"avoid": true,
|
||||
"embeddedRelay": true,
|
||||
"nodes": [
|
||||
{
|
||||
"certName": "string",
|
||||
"derpport": 0,
|
||||
"forceHTTP": true,
|
||||
"hostName": "string",
|
||||
"insecureForTests": true,
|
||||
"ipv4": "string",
|
||||
"ipv6": "string",
|
||||
"name": "string",
|
||||
"regionID": 0,
|
||||
"stunonly": true,
|
||||
"stunport": 0,
|
||||
"stuntestIP": "string"
|
||||
}
|
||||
],
|
||||
"regionCode": "string",
|
||||
"regionID": 0,
|
||||
"regionName": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pass": true,
|
||||
"time": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------ | ------------------------------------------------ | -------- | ------------ | ------------------------------------------------ |
|
||||
| `derp` | [healthcheck.DERPReport](#healthcheckderpreport) | false | | |
|
||||
| `pass` | boolean | false | | Healthy is true if the report returns no errors. |
|
||||
| `time` | string | false | | Time is the time the report was generated at. |
|
||||
|
||||
## netcheck.Report
|
||||
|
||||
```json
|
||||
{
|
||||
"captivePortal": "string",
|
||||
"globalV4": "string",
|
||||
"globalV6": "string",
|
||||
"hairPinning": "string",
|
||||
"icmpv4": true,
|
||||
"ipv4": true,
|
||||
"ipv4CanSend": true,
|
||||
"ipv6": true,
|
||||
"ipv6CanSend": true,
|
||||
"mappingVariesByDestIP": "string",
|
||||
"oshasIPv6": true,
|
||||
"pcp": "string",
|
||||
"pmp": "string",
|
||||
"preferredDERP": 0,
|
||||
"regionLatency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV4Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"regionV6Latency": {
|
||||
"property1": 0,
|
||||
"property2": 0
|
||||
},
|
||||
"udp": true,
|
||||
"upnP": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ----------------------- | ------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `captivePortal` | string | false | | Captiveportal is set when we think there's a captive portal that is intercepting HTTP traffic. |
|
||||
| `globalV4` | string | false | | ip:port of global IPv4 |
|
||||
| `globalV6` | string | false | | [ip]:port of global IPv6 |
|
||||
| `hairPinning` | string | false | | Hairpinning is whether the router supports communicating between two local devices through the NATted public IP address (on IPv4). |
|
||||
| `icmpv4` | boolean | false | | an ICMPv4 round trip completed |
|
||||
| `ipv4` | boolean | false | | an IPv4 STUN round trip completed |
|
||||
| `ipv4CanSend` | boolean | false | | an IPv4 packet was able to be sent |
|
||||
| `ipv6` | boolean | false | | an IPv6 STUN round trip completed |
|
||||
| `ipv6CanSend` | boolean | false | | an IPv6 packet was able to be sent |
|
||||
| `mappingVariesByDestIP` | string | false | | Mappingvariesbydestip is whether STUN results depend which STUN server you're talking to (on IPv4). |
|
||||
| `oshasIPv6` | boolean | false | | could bind a socket to ::1 |
|
||||
| `pcp` | string | false | | Pcp is whether PCP appears present on the LAN. Empty means not checked. |
|
||||
| `pmp` | string | false | | Pmp is whether NAT-PMP appears present on the LAN. Empty means not checked. |
|
||||
| `preferredDERP` | integer | false | | or 0 for unknown |
|
||||
| `regionLatency` | object | false | | keyed by DERP Region ID |
|
||||
| » `[any property]` | integer | false | | |
|
||||
| `regionV4Latency` | object | false | | keyed by DERP Region ID |
|
||||
| » `[any property]` | integer | false | | |
|
||||
| `regionV6Latency` | object | false | | keyed by DERP Region ID |
|
||||
| » `[any property]` | integer | false | | |
|
||||
| `udp` | boolean | false | | a UDP STUN round trip completed |
|
||||
| `upnP` | string | false | | Upnp is whether UPnP appears present on the LAN. Empty means not checked. |
|
||||
|
||||
## parameter.ComputedValue
|
||||
|
||||
```json
|
||||
|
||||
@@ -36,7 +36,7 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0
|
||||
|
||||
// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
|
||||
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
|
||||
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230327205451-058fa46a3723
|
||||
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20230329230537-76a675d945b7
|
||||
|
||||
// Switch to our fork that imports fixes from http://github.com/tailscale/ssh.
|
||||
// See: https://github.com/coder/coder/issues/3371
|
||||
|
||||
@@ -380,8 +380,8 @@ github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d h1:09JG37IgTB6n3ouX9
|
||||
github.com/coder/retry v1.3.1-0.20230210155434-e90a2e1e091d/go.mod h1:r+1J5i/989wt6CUeNSuvFKKA9hHuKKPMxdzDbTuvwwk=
|
||||
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHuiMNJ0qlhoD3pGN3JY9gxSko=
|
||||
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||
github.com/coder/tailscale v1.1.1-0.20230327205451-058fa46a3723 h1:TIEdewTbti0kTXo6kgKpF1MD3w+RYH+BZfkD1BqVB8c=
|
||||
github.com/coder/tailscale v1.1.1-0.20230327205451-058fa46a3723/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
|
||||
github.com/coder/tailscale v1.1.1-0.20230329230537-76a675d945b7 h1:07rCDmnCKGG666bR0WI1grlI/RnI54xj0C+2d2gWeE4=
|
||||
github.com/coder/tailscale v1.1.1-0.20230329230537-76a675d945b7/go.mod h1:jpg+77g19FpXL43U1VoIqoSg1K/Vh5CVxycGldQ8KhA=
|
||||
github.com/coder/terraform-provider-coder v0.6.21 h1:TIH6+/VQFreT8q/CkRvpHtbIeM5cOAhuDS5Sh1Nm21Q=
|
||||
github.com/coder/terraform-provider-coder v0.6.21/go.mod h1:UIfU3bYNeSzJJvHyJ30tEKjD6Z9utloI+HUM/7n94CY=
|
||||
github.com/coder/wgtunnel v0.1.5 h1:WP3sCj/3iJ34eKvpMQEp1oJHvm24RYh0NHbj1kfUKfs=
|
||||
|
||||
Reference in New Issue
Block a user