diff --git a/cli/server.go b/cli/server.go index 4ebee17d7e..925a1a619a 100644 --- a/cli/server.go +++ b/cli/server.go @@ -81,6 +81,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command { derpServerRegionName string derpServerSTUNAddrs []string derpConfigURL string + derpConfigPath string promEnabled bool promAddress string pprofEnabled bool @@ -345,7 +346,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command { if !derpServerEnabled { defaultRegion = nil } - derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, derpServerSTUNAddrs, derpConfigURL) + derpMap, err := tailnet.NewDERPMap(ctx, defaultRegion, derpServerSTUNAddrs, derpConfigURL, derpConfigPath) if err != nil { return xerrors.Errorf("create derp map: %w", err) } @@ -753,6 +754,8 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command { cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard.") cliflag.StringVarP(root.Flags(), &derpConfigURL, "derp-config-url", "", "CODER_DERP_CONFIG_URL", "", "Specifies a URL to periodically fetch a DERP map. See: https://tailscale.com/kb/1118/custom-derp-servers/") + cliflag.StringVarP(root.Flags(), &derpConfigPath, "derp-config-path", "", "CODER_DERP_CONFIG_PATH", "", + "Specifies a path to read a DERP map from. See: https://tailscale.com/kb/1118/custom-derp-servers/") cliflag.BoolVarP(root.Flags(), &derpServerEnabled, "derp-server-enable", "", "CODER_DERP_SERVER_ENABLE", true, "Specifies whether to enable or disable the embedded DERP server.") cliflag.IntVarP(root.Flags(), &derpServerRegionID, "derp-server-region-id", "", "CODER_DERP_SERVER_REGION_ID", 999, "Specifies the region ID to use for the embedded DERP server.") cliflag.StringVarP(root.Flags(), &derpServerRegionCode, "derp-server-region-code", "", "CODER_DERP_SERVER_REGION_CODE", "coder", "Specifies the region code that is displayed in the Coder UI for the embedded DERP server.") @@ -763,6 +766,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command { // Mark hidden while this feature is in testing! _ = root.Flags().MarkHidden("derp-config-url") + _ = root.Flags().MarkHidden("derp-config-path") _ = root.Flags().MarkHidden("derp-server-enable") _ = root.Flags().MarkHidden("derp-server-region-id") _ = root.Flags().MarkHidden("derp-server-region-code") diff --git a/cli/speedtest.go b/cli/speedtest.go index 1656112105..ab1586815b 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -11,6 +11,7 @@ import ( tsspeedtest "tailscale.com/net/speedtest" "cdr.dev/slog" + "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/agent" "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/cliui" @@ -51,36 +52,43 @@ func speedtest() *cobra.Command { if err != nil { return xerrors.Errorf("await agent: %w", err) } - conn, err := client.DialWorkspaceAgentTailnet(ctx, slog.Logger{}, workspaceAgent.ID) + logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) + if cliflag.IsSetBool(cmd, varVerbose) { + logger = logger.Leveled(slog.LevelDebug) + } + conn, err := client.DialWorkspaceAgentTailnet(ctx, logger, workspaceAgent.ID) if err != nil { return err } defer conn.Close() - if direct { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-ticker.C: - } - dur, err := conn.Ping() - if err != nil { - continue - } - tc, _ := conn.(*agent.TailnetConn) - status := tc.Status() - if len(status.Peers()) != 1 { - continue - } - peer := status.Peer[status.Peers()[0]] - if peer.CurAddr == "" { - cmd.Printf("Waiting for a direct connection... (%dms via %s)\n", dur.Milliseconds(), peer.Relay) - continue - } - break + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: } + dur, err := conn.Ping() + if err != nil { + continue + } + tc, _ := conn.(*agent.TailnetConn) + status := tc.Status() + if len(status.Peers()) != 1 { + continue + } + peer := status.Peer[status.Peers()[0]] + if peer.CurAddr == "" && direct { + cmd.Printf("Waiting for a direct connection... (%dms via %s)\n", dur.Milliseconds(), peer.Relay) + continue + } + via := peer.Relay + if via == "" { + via = "direct" + } + cmd.Printf("%dms via %s\n", dur.Milliseconds(), via) + break } dir := tsspeedtest.Download if reverse { diff --git a/tailnet/conn.go b/tailnet/conn.go index 9e5134e4d6..10654b02ac 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -268,6 +268,7 @@ func (c *Conn) SetNodeCallback(callback func(node *Node)) { case <-c.closed: return case node := <-queue: + c.logger.Debug(context.Background(), "send node callback", slog.F("node", node)) callback(node) } } @@ -299,6 +300,8 @@ func (c *Conn) SetNodeCallback(callback func(node *Node)) { func (c *Conn) SetDERPMap(derpMap *tailcfg.DERPMap) { c.mutex.Lock() defer c.mutex.Unlock() + c.netMap.DERPMap = derpMap + c.logger.Debug(context.Background(), "updating derp map", slog.F("derp_map", derpMap)) c.wireguardEngine.SetDERPMap(derpMap) } @@ -340,6 +343,9 @@ func (c *Conn) UpdateNodes(nodes []*Node) error { existingNode, ok := peerMap[node.ID] if ok { peerNode.Created = existingNode.Created + c.logger.Debug(context.Background(), "updating peer", slog.F("peer", peerNode)) + } else { + c.logger.Debug(context.Background(), "adding peer", slog.F("peer", peerNode)) } peerMap[node.ID] = peerNode } diff --git a/tailnet/derpmap.go b/tailnet/derpmap.go index cbc03cec41..13a998177c 100644 --- a/tailnet/derpmap.go +++ b/tailnet/derpmap.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/http" + "os" "strconv" "golang.org/x/xerrors" @@ -14,7 +15,10 @@ import ( // NewDERPMap constructs a DERPMap from a set of STUN addresses and optionally a remote // URL to fetch a mapping from e.g. https://controlplane.tailscale.com/derpmap/default. -func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []string, remoteURL string) (*tailcfg.DERPMap, error) { +func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []string, remoteURL, localPath string) (*tailcfg.DERPMap, error) { + if remoteURL != "" && localPath != "" { + return nil, xerrors.New("a remote URL or local path must be specified, not both") + } if region != nil { for index, stunAddr := range stunAddrs { host, rawPort, err := net.SplitHostPort(stunAddr) @@ -53,6 +57,16 @@ func NewDERPMap(ctx context.Context, region *tailcfg.DERPRegion, stunAddrs []str return nil, xerrors.Errorf("fetch derpmap: %w", err) } } + if localPath != "" { + content, err := os.ReadFile(localPath) + if err != nil { + return nil, xerrors.Errorf("read derpmap from %q: %w", localPath, err) + } + err = json.Unmarshal(content, &derpMap) + if err != nil { + return nil, xerrors.Errorf("unmarshal derpmap: %w", err) + } + } if region != nil { _, conflicts := derpMap.Regions[region.RegionID] if conflicts { diff --git a/tailnet/derpmap_test.go b/tailnet/derpmap_test.go index 252ccef907..91c1a88a85 100644 --- a/tailnet/derpmap_test.go +++ b/tailnet/derpmap_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -20,7 +22,7 @@ func TestNewDERPMap(t *testing.T) { derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{ RegionID: 1, Nodes: []*tailcfg.DERPNode{{}}, - }, []string{"stun.google.com:2345"}, "") + }, []string{"stun.google.com:2345"}, "", "") require.NoError(t, err) require.Len(t, derpMap.Regions[1].Nodes, 2) }) @@ -37,7 +39,7 @@ func TestNewDERPMap(t *testing.T) { t.Cleanup(server.Close) derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{ RegionID: 2, - }, []string{}, server.URL) + }, []string{}, server.URL, "") require.NoError(t, err) require.Len(t, derpMap.Regions, 2) }) @@ -54,7 +56,24 @@ func TestNewDERPMap(t *testing.T) { t.Cleanup(server.Close) _, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{ RegionID: 1, - }, []string{}, server.URL) + }, []string{}, server.URL, "") require.Error(t, err) }) + t.Run("LocalPath", func(t *testing.T) { + t.Parallel() + localPath := filepath.Join(t.TempDir(), "derp.json") + content, err := json.Marshal(&tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: {}, + }, + }) + require.NoError(t, err) + err = os.WriteFile(localPath, content, 0600) + require.NoError(t, err) + derpMap, err := tailnet.NewDERPMap(context.Background(), &tailcfg.DERPRegion{ + RegionID: 2, + }, []string{}, "", localPath) + require.NoError(t, err) + require.Len(t, derpMap.Regions, 2) + }) }