mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1237 lines
28 KiB
Go
1237 lines
28 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_debug
|
|
|
|
package localapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
// mockBackendForDERP implements the subset of LocalBackend methods needed for DERP tests
|
|
type mockBackendForDERP struct {
|
|
ipnlocal.NoOpBackend
|
|
derpMap *tailcfg.DERPMap
|
|
}
|
|
|
|
func (m *mockBackendForDERP) DERPMap() *tailcfg.DERPMap {
|
|
return m.derpMap
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_PermissionDenied tests permission check
|
|
func TestServeDebugDERPRegion_PermissionDenied(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: false,
|
|
b: &mockBackendForDERP{},
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "debug access denied") {
|
|
t.Errorf("body = %q, want access denied error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_MethodNotAllowed tests POST requirement
|
|
func TestServeDebugDERPRegion_MethodNotAllowed(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{},
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "POST required") {
|
|
t.Errorf("body = %q, want POST required error", body)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NoDERPMap tests nil DERP map
|
|
func TestServeDebugDERPRegion_NoDERPMap(t *testing.T) {
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{}, // nil derpMap
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
// Always returns JSON, even on error
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no DERP map
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about no DERP map")
|
|
}
|
|
|
|
if !strings.Contains(report.Errors[0], "no DERP map") {
|
|
t.Errorf("error = %q, want no DERP map error", report.Errors[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NoSuchRegionByID tests non-existent region ID
|
|
func TestServeDebugDERPRegion_NoSuchRegionByID(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test1.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=999", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about non-existent region")
|
|
}
|
|
|
|
if !strings.Contains(report.Errors[0], "no such region") {
|
|
t.Errorf("error = %q, want no such region error", report.Errors[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NoSuchRegionByCode tests non-existent region code
|
|
func TestServeDebugDERPRegion_NoSuchRegionByCode(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "nyc",
|
|
RegionName: "New York",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "nyc1.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=sfo", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about non-existent region")
|
|
}
|
|
|
|
if !strings.Contains(report.Errors[0], "no such region") {
|
|
t.Errorf("error = %q, want no such region error", report.Errors[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_FindByRegionID tests finding region by numeric ID
|
|
func TestServeDebugDERPRegion_FindByRegionID(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test1.example.com",
|
|
IPv4: "1.2.3.4",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have info about the region
|
|
if len(report.Info) == 0 {
|
|
t.Error("expected info messages about region")
|
|
}
|
|
|
|
// First info should identify the region
|
|
if !strings.Contains(report.Info[0], "Region 1") {
|
|
t.Errorf("info[0] = %q, want region info", report.Info[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_FindByRegionCode tests finding region by code
|
|
func TestServeDebugDERPRegion_FindByRegionCode(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "nyc",
|
|
RegionName: "New York",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "nyc1.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
2: {
|
|
RegionID: 2,
|
|
RegionCode: "sfo",
|
|
RegionName: "San Francisco",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "2a",
|
|
RegionID: 2,
|
|
HostName: "sfo1.example.com",
|
|
IPv4: "192.0.2.2",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=sfo", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have info about the SFO region
|
|
if len(report.Info) == 0 {
|
|
t.Fatal("expected info messages about region")
|
|
}
|
|
|
|
// First info should identify the region
|
|
if !strings.Contains(report.Info[0], "Region 2") || !strings.Contains(report.Info[0], "sfo") {
|
|
t.Errorf("info[0] = %q, want sfo region info", report.Info[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_SingleRegionWarning tests warning for single region
|
|
func TestServeDebugDERPRegion_SingleRegionWarning(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "only",
|
|
RegionName: "Only Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "only.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have warning about single region
|
|
if len(report.Warnings) == 0 {
|
|
t.Fatal("expected warnings about single region")
|
|
}
|
|
|
|
found := false
|
|
for _, w := range report.Warnings {
|
|
if strings.Contains(w, "single DERP region") && strings.Contains(w, "single point of failure") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("warnings = %v, want single region warning", report.Warnings)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_MultipleRegionsNoWarning tests no warning for multiple regions
|
|
func TestServeDebugDERPRegion_MultipleRegionsNoWarning(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "nyc",
|
|
RegionName: "New York",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "nyc.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
2: {
|
|
RegionID: 2,
|
|
RegionCode: "sfo",
|
|
RegionName: "San Francisco",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "2a",
|
|
RegionID: 2,
|
|
HostName: "sfo.example.com",
|
|
IPv4: "192.0.2.2",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should NOT have warning about single region
|
|
for _, w := range report.Warnings {
|
|
if strings.Contains(w, "single DERP region") {
|
|
t.Errorf("unexpected single region warning: %q", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_AvoidBitWarning tests warning for Avoid bit
|
|
func TestServeDebugDERPRegion_AvoidBitWarning(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "avoid",
|
|
RegionName: "Avoided Region",
|
|
Avoid: true,
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "avoid.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
2: {
|
|
RegionID: 2,
|
|
RegionCode: "ok",
|
|
RegionName: "OK Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "2a",
|
|
RegionID: 2,
|
|
HostName: "ok.example.com",
|
|
IPv4: "192.0.2.2",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have warning about Avoid bit
|
|
found := false
|
|
for _, w := range report.Warnings {
|
|
if strings.Contains(w, "marked with Avoid bit") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("warnings = %v, want Avoid bit warning", report.Warnings)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NoAvoidBit tests no warning when Avoid is false
|
|
func TestServeDebugDERPRegion_NoAvoidBit(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "ok",
|
|
RegionName: "OK Region",
|
|
Avoid: false,
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "ok.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should NOT have Avoid bit warning
|
|
for _, w := range report.Warnings {
|
|
if strings.Contains(w, "Avoid bit") {
|
|
t.Errorf("unexpected Avoid bit warning: %q", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NoNodesError tests error for region with no nodes
|
|
func TestServeDebugDERPRegion_NoNodesError(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "empty",
|
|
RegionName: "Empty Region",
|
|
Nodes: []*tailcfg.DERPNode{}, // Empty!
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no nodes
|
|
if len(report.Errors) == 0 {
|
|
t.Fatal("expected errors about no nodes")
|
|
}
|
|
|
|
found := false
|
|
for _, e := range report.Errors {
|
|
if strings.Contains(e, "no nodes defined") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("errors = %v, want no nodes error", report.Errors)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NilNodesError tests error for nil nodes
|
|
func TestServeDebugDERPRegion_NilNodesError(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "nil",
|
|
RegionName: "Nil Nodes Region",
|
|
Nodes: nil, // nil!
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no nodes
|
|
if len(report.Errors) == 0 {
|
|
t.Fatal("expected errors about no nodes")
|
|
}
|
|
|
|
found := false
|
|
for _, e := range report.Errors {
|
|
if strings.Contains(e, "no nodes") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("errors = %v, want no nodes error", report.Errors)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_STUNOnlyNodeInfo tests info for STUN-only nodes
|
|
func TestServeDebugDERPRegion_STUNOnlyNodeInfo(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "stun",
|
|
RegionName: "STUN Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "stun.example.com",
|
|
IPv4: "192.0.2.1",
|
|
STUNOnly: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have info about STUNOnly node
|
|
found := false
|
|
for _, i := range report.Info {
|
|
if strings.Contains(i, "STUNOnly") {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("info = %v, want STUNOnly info", report.Info)
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_EmptyRegionParameter tests empty region parameter
|
|
func TestServeDebugDERPRegion_EmptyRegionParameter(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no such region
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about empty region parameter")
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_MissingRegionParameter tests missing region parameter
|
|
func TestServeDebugDERPRegion_MissingRegionParameter(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no such region
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about missing region parameter")
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_ResponseStructure tests the response structure
|
|
func TestServeDebugDERPRegion_ResponseStructure(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
// Verify Content-Type
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", contentType)
|
|
}
|
|
|
|
// Verify response can be decoded
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Report should have at least Info about the region
|
|
if len(report.Info) == 0 {
|
|
t.Error("expected at least one info message")
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_MultipleNodes tests region with multiple nodes
|
|
func TestServeDebugDERPRegion_MultipleNodes(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "multi",
|
|
RegionName: "Multi-Node Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "node1.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
{
|
|
Name: "1b",
|
|
RegionID: 1,
|
|
HostName: "node2.example.com",
|
|
IPv4: "192.0.2.2",
|
|
},
|
|
{
|
|
Name: "1c",
|
|
RegionID: 1,
|
|
HostName: "node3.example.com",
|
|
IPv4: "192.0.2.3",
|
|
STUNOnly: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have info about the region
|
|
if len(report.Info) == 0 {
|
|
t.Error("expected info messages")
|
|
}
|
|
|
|
// With multiple nodes, there will be errors trying to connect
|
|
// (since this is a test environment), but that's expected
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_RegionIDZero tests region ID 0
|
|
func TestServeDebugDERPRegion_RegionIDZero(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
0: {
|
|
RegionID: 0,
|
|
RegionCode: "zero",
|
|
RegionName: "Zero Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "0a",
|
|
RegionID: 0,
|
|
HostName: "zero.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=0", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should find region 0
|
|
if len(report.Info) == 0 {
|
|
t.Fatal("expected info messages about region 0")
|
|
}
|
|
|
|
if !strings.Contains(report.Info[0], "Region 0") {
|
|
t.Errorf("info[0] = %q, want region 0 info", report.Info[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_NegativeRegionID tests negative region ID
|
|
func TestServeDebugDERPRegion_NegativeRegionID(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "test",
|
|
RegionName: "Test",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "test.example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=-1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no such region
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about non-existent region")
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_VeryLargeRegionID tests very large region ID
|
|
func TestServeDebugDERPRegion_VeryLargeRegionID(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
999999: {
|
|
RegionID: 999999,
|
|
RegionCode: "huge",
|
|
RegionName: "Huge ID Region",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "999999a",
|
|
RegionID: 999999,
|
|
HostName: "huge.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=999999", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should find the region
|
|
if len(report.Info) == 0 {
|
|
t.Fatal("expected info messages")
|
|
}
|
|
|
|
if !strings.Contains(report.Info[0], "999999") {
|
|
t.Errorf("info[0] = %q, want region 999999 info", report.Info[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_SpecialCharactersInRegionCode tests special characters
|
|
func TestServeDebugDERPRegion_SpecialCharactersInRegionCode(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "us-west-2",
|
|
RegionName: "US West 2",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "us-west-2.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: logger.Discard,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=us-west-2", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should find the region
|
|
if len(report.Info) == 0 {
|
|
t.Fatal("expected info messages")
|
|
}
|
|
|
|
if !strings.Contains(report.Info[0], "us-west-2") {
|
|
t.Errorf("info[0] = %q, want us-west-2 info", report.Info[0])
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_CaseSensitiveRegionCode tests case sensitivity
|
|
func TestServeDebugDERPRegion_CaseSensitiveRegionCode(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{
|
|
1: {
|
|
RegionID: 1,
|
|
RegionCode: "NYC",
|
|
RegionName: "New York",
|
|
Nodes: []*tailcfg.DERPNode{
|
|
{
|
|
Name: "1a",
|
|
RegionID: 1,
|
|
HostName: "nyc.example.com",
|
|
IPv4: "192.0.2.1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
// Try lowercase when region code is uppercase
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=nyc", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should NOT find the region (case-sensitive)
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about non-existent region (case mismatch)")
|
|
}
|
|
}
|
|
|
|
// TestServeDebugDERPRegion_EmptyDERPMap tests empty DERP map
|
|
func TestServeDebugDERPRegion_EmptyDERPMap(t *testing.T) {
|
|
derpMap := &tailcfg.DERPMap{
|
|
Regions: map[int]*tailcfg.DERPRegion{},
|
|
}
|
|
|
|
h := &Handler{
|
|
PermitWrite: true,
|
|
b: &mockBackendForDERP{
|
|
derpMap: derpMap,
|
|
},
|
|
logf: t.Logf,
|
|
}
|
|
|
|
req := httptest.NewRequest("POST", "/localapi/v0/debug-derp-region?region=1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.serveDebugDERPRegion(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var report ipnstate.DebugDERPRegionReport
|
|
if err := json.NewDecoder(w.Body).Decode(&report); err != nil {
|
|
t.Fatalf("failed to decode JSON: %v", err)
|
|
}
|
|
|
|
// Should have error about no such region
|
|
if len(report.Errors) == 0 {
|
|
t.Error("expected errors about non-existent region")
|
|
}
|
|
}
|