diff --git a/cli/server_test.go b/cli/server_test.go index 7034f2fa33..0db409af49 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1755,3 +1755,22 @@ func TestConnectToPostgres(t *testing.T) { }) require.NoError(t, sqlDB.PingContext(ctx)) } + +func TestServer_InvalidDERP(t *testing.T) { + t.Parallel() + + // Try to start a server with the built-in DERP server disabled and no + // external DERP map. + inv, _ := clitest.New(t, + "server", + "--in-memory", + "--http-address", ":0", + "--access-url", "http://example.com", + "--derp-server-enable=false", + "--derp-server-stun-addresses", "disable", + "--block-direct-connections", + ) + err := inv.Run() + require.Error(t, err) + require.ErrorContains(t, err, "A valid DERP map is required for networking to work") +} diff --git a/tailnet/derpmap.go b/tailnet/derpmap.go index 817ef6a79d..e2722c1ff9 100644 --- a/tailnet/derpmap.go +++ b/tailnet/derpmap.go @@ -130,6 +130,25 @@ func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []str } } + // Fail if the DERP map has no regions or no DERP nodes. + badDerpMapMsg := "A valid DERP map is required for networking to work. You must either supply your own DERP map or use the built-in DERP server" + if len(derpMap.Regions) == 0 { + return nil, xerrors.New("DERP map has no regions. " + badDerpMapMsg) + } + foundValidNode := false +regionLoop: + for _, region := range derpMap.Regions { + for _, node := range region.Nodes { + if !node.STUNOnly { + foundValidNode = true + break regionLoop + } + } + } + if !foundValidNode { + return nil, xerrors.New("DERP map has no DERP nodes. " + badDerpMapMsg) + } + return derpMap, nil } diff --git a/tailnet/derpmap_test.go b/tailnet/derpmap_test.go index cdcea8e236..a91969bfec 100644 --- a/tailnet/derpmap_test.go +++ b/tailnet/derpmap_test.go @@ -33,7 +33,10 @@ func TestNewDERPMap(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := json.Marshal(&tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ - 1: {}, + 1: { + RegionID: 1, + Nodes: []*tailcfg.DERPNode{{}}, + }, }, }) _, _ = w.Write(data) @@ -66,7 +69,9 @@ func TestNewDERPMap(t *testing.T) { localPath := filepath.Join(t.TempDir(), "derp.json") content, err := json.Marshal(&tailcfg.DERPMap{ Regions: map[int]*tailcfg.DERPRegion{ - 1: {}, + 1: { + Nodes: []*tailcfg.DERPNode{{}}, + }, }, }) require.NoError(t, err) @@ -131,4 +136,29 @@ func TestNewDERPMap(t *testing.T) { // region. require.EqualValues(t, -1, derpMap.Regions[3].Nodes[0].STUNPort) }) + t.Run("RequireRegions", func(t *testing.T) { + t.Parallel() + _, err := tailnet.NewDERPMap(context.Background(), nil, nil, "", "", false) + require.Error(t, err) + require.ErrorContains(t, err, "DERP map has no regions") + }) + t.Run("RequireDERPNodes", func(t *testing.T) { + t.Parallel() + + // No nodes. + _, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{}, nil, "", "", false) + require.Error(t, err) + require.ErrorContains(t, err, "DERP map has no DERP nodes") + + // No DERP nodes. + _, err = tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{ + Nodes: []*tailcfg.DERPNode{ + { + STUNOnly: true, + }, + }, + }, nil, "", "", false) + require.Error(t, err) + require.ErrorContains(t, err, "DERP map has no DERP nodes") + }) }