diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index f32771488..30ef2db3c 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -24,6 +24,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/paths" "tailscale.com/safesocket" + "tailscale.com/tailcfg" ) // TailscaledSocket is the tailscaled Unix socket. @@ -278,3 +279,17 @@ func SetDNS(ctx context.Context, name, value string) error { _, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil) return err } + +// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled. +// It is intended to be used with netcheck to see availability of DERPs. +func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) { + var derpMap tailcfg.DERPMap + res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil) + if err != nil { + return nil, err + } + if err = json.Unmarshal(res, &derpMap); err != nil { + return nil, fmt.Errorf("invalid derp map json: %w", err) + } + return &derpMap, nil +} diff --git a/cmd/tailscale/cli/netcheck.go b/cmd/tailscale/cli/netcheck.go index 09ce664cd..90a5815e8 100644 --- a/cmd/tailscale/cli/netcheck.go +++ b/cmd/tailscale/cli/netcheck.go @@ -9,14 +9,18 @@ "encoding/json" "flag" "fmt" + "io" + "io/ioutil" "log" + "net/http" "os" "sort" "strings" "time" "github.com/peterbourgon/ff/v2/ffcli" - "tailscale.com/derp/derpmap" + "tailscale.com/client/tailscale" + "tailscale.com/ipn" "tailscale.com/net/netcheck" "tailscale.com/net/portmapper" "tailscale.com/tailcfg" @@ -59,7 +63,13 @@ func runNetcheck(ctx context.Context, args []string) error { fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface") } - dm := derpmap.Prod() + dm, err := tailscale.CurrentDERPMap(ctx) + if err != nil { + dm, err = prodDERPMap(ctx, http.DefaultClient) + if err != nil { + return err + } + } for { t0 := time.Now() report, err := c.GetReport(ctx, dm) @@ -176,3 +186,27 @@ func portMapping(r *netcheck.Report) string { } return strings.Join(got, ", ") } + +func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, error) { + req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil) + if err != nil { + return nil, fmt.Errorf("create prodDERPMap request: %w", err) + } + res, err := httpc.Do(req) + if err != nil { + return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err) + } + defer res.Body.Close() + b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err) + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("fetch prodDERPMap: %v: %s", res.Status, b) + } + var derpMap tailcfg.DERPMap + if err = json.Unmarshal(b, &derpMap); err != nil { + return nil, fmt.Errorf("fetch prodDERPMap: %w", err) + } + return &derpMap, nil +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 1ba02d46e..26a459de7 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -20,7 +20,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale tailscale.com/derp from tailscale.com/derp/derphttp tailscale.com/derp/derphttp from tailscale.com/net/netcheck - tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli tailscale.com/disco from tailscale.com/derp tailscale.com/hostinfo from tailscale.com/net/interfaces tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+ diff --git a/cmd/tailscaled/debug.go b/cmd/tailscaled/debug.go index 3419dfb3b..141c3814a 100644 --- a/cmd/tailscaled/debug.go +++ b/cmd/tailscaled/debug.go @@ -11,6 +11,8 @@ "errors" "flag" "fmt" + "io" + "io/ioutil" "log" "net/http" "net/http/httptrace" @@ -19,7 +21,7 @@ "time" "tailscale.com/derp/derphttp" - "tailscale.com/derp/derpmap" + "tailscale.com/ipn" "tailscale.com/net/interfaces" "tailscale.com/net/tshttpproxy" "tailscale.com/tailcfg" @@ -131,7 +133,26 @@ func getURL(ctx context.Context, urlStr string) error { } func checkDerp(ctx context.Context, derpRegion string) error { - dmap := derpmap.Prod() + req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil) + if err != nil { + return fmt.Errorf("create derp map request: %w", err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("fetch derp map failed: %w", err) + } + defer res.Body.Close() + b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20)) + if err != nil { + return fmt.Errorf("fetch derp map failed: %w", err) + } + if res.StatusCode != 200 { + return fmt.Errorf("fetch derp map: %v: %s", res.Status, b) + } + var dmap tailcfg.DERPMap + if err = json.Unmarshal(b, &dmap); err != nil { + return fmt.Errorf("fetch DERP map: %w", err) + } getRegion := func() *tailcfg.DERPRegion { for _, r := range dmap.Regions { if r.RegionCode == derpRegion { diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 381cef161..b5046726c 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -81,7 +81,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ tailscale.com/derp from tailscale.com/derp/derphttp+ tailscale.com/derp/derphttp from tailscale.com/net/netcheck+ - tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled tailscale.com/disco from tailscale.com/derp+ tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/hostinfo from tailscale.com/control/controlclient+ diff --git a/derp/derpmap/derpmap.go b/derp/derpmap/derpmap.go deleted file mode 100644 index aec468015..000000000 --- a/derp/derpmap/derpmap.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package derpmap contains information about Tailscale.com's production DERP nodes. -// -// This package is only used by the "tailscale netcheck" command for debugging. -// In normal operation the Tailscale nodes get this sent to them from the control -// server. -// -// TODO: remove this package and make "tailscale netcheck" get the -// list from the control server too. -package derpmap - -import ( - "fmt" - "strings" - - "tailscale.com/tailcfg" -) - -func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode { - return &tailcfg.DERPNode{ - Name: suffix, // updated later - RegionID: 0, // updated later - IPv4: v4, - IPv6: v6, - } -} - -func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion { - region := &tailcfg.DERPRegion{ - RegionID: id, - RegionName: name, - RegionCode: code, - Nodes: nodes, - } - for _, n := range nodes { - n.Name = fmt.Sprintf("%d%s", id, n.Name) - n.RegionID = id - n.HostName = fmt.Sprintf("derp%s.tailscale.com", strings.TrimSuffix(n.Name, "a")) - } - return region -} - -// Prod returns Tailscale's map of relay servers. -// -// This list is only used by cmd/tailscale's netcheck subcommand. In -// normal operation the Tailscale nodes get this sent to them from the -// control server. -// -// This list is subject to change and should not be relied on. -func Prod() *tailcfg.DERPMap { - return &tailcfg.DERPMap{ - Regions: map[int]*tailcfg.DERPRegion{ - 1: derpRegion(1, "nyc", "New York City", - derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"), - ), - 2: derpRegion(2, "sfo", "San Francisco", - derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"), - ), - 3: derpRegion(3, "sin", "Singapore", - derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"), - ), - 4: derpRegion(4, "fra", "Frankfurt", - derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"), - ), - 5: derpRegion(5, "syd", "Sydney", - derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"), - ), - 6: derpRegion(6, "blr", "Bangalore", - derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"), - ), - 7: derpRegion(7, "tok", "Tokyo", - derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"), - ), - 8: derpRegion(8, "lhr", "London", - derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"), - ), - 9: derpRegion(9, "dfw", "Dallas", - derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"), - ), - 10: derpRegion(10, "sea", "Seattle", - derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"), - ), - 11: derpRegion(11, "sao", "São Paulo", - derpNode("a", "18.230.97.74", "2600:1f1e:ee4:5611:ec5c:1736:d43b:a454"), - ), - }, - } -} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index f669a336e..fd777b0cb 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -2752,3 +2752,13 @@ func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c sys } return nil } + +// DERPMap returns the current DERPMap in use, or nil if not connected. +func (b *LocalBackend) DERPMap() *tailcfg.DERPMap { + b.mu.Lock() + defer b.mu.Unlock() + if b.netMap == nil { + return nil + } + return b.netMap.DERPMap +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index c9a5ed029..49c05f214 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -102,6 +102,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveFileTargets(w, r) case "/localapi/v0/set-dns": h.serveSetDNS(w, r) + case "/localapi/v0/derpmap": + h.serveDERPMap(w, r) case "/": io.WriteString(w, "tailscaled\n") default: @@ -403,6 +405,17 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(struct{}{}) } +func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "want GET", 400) + return + } + w.Header().Set("Content-Type", "application/json") + e := json.NewEncoder(w) + e.SetIndent("", "\t") + e.Encode(h.b.DERPMap()) +} + var dialPeerTransportOnce struct { sync.Once v *http.Transport diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index 6915db958..1870517d6 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -28,7 +28,6 @@ "github.com/klauspost/compress/zstd" "golang.org/x/crypto/nacl/box" "inet.af/netaddr" - "tailscale.com/derp/derpmap" "tailscale.com/smallzstd" "tailscale.com/tailcfg" "tailscale.com/types/logger" @@ -616,8 +615,6 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.M KeepAlive: true, } -var prodDERPMap = derpmap.Prod() - // MapResponse generates a MapResponse for a MapRequest. // // No updates to s are done here. @@ -627,14 +624,10 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, // node key rotated away (once test server supports that) return nil, nil } - derpMap := s.DERPMap - if derpMap == nil { - derpMap = prodDERPMap - } user, _ := s.getUser(req.NodeKey) res = &tailcfg.MapResponse{ Node: node, - DERPMap: derpMap, + DERPMap: s.DERPMap, Domain: string(user.Domain), CollectServices: "true", PacketFilter: tailcfg.FilterAllowAll, diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index a54c69470..d892b92cf 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -32,7 +32,6 @@ "inet.af/netaddr" "tailscale.com/derp" "tailscale.com/derp/derphttp" - "tailscale.com/derp/derpmap" "tailscale.com/ipn/ipnstate" "tailscale.com/net/stun/stuntest" "tailscale.com/net/tstun" @@ -396,7 +395,19 @@ func TestPickDERPFallback(t *testing.T) { tstest.ResourceCheck(t) c := newConn() - c.derpMap = derpmap.Prod() + dm := &tailcfg.DERPMap{ + Regions: map[int]*tailcfg.DERPRegion{ + 1: &tailcfg.DERPRegion{}, + 2: &tailcfg.DERPRegion{}, + 3: &tailcfg.DERPRegion{}, + 4: &tailcfg.DERPRegion{}, + 5: &tailcfg.DERPRegion{}, + 6: &tailcfg.DERPRegion{}, + 7: &tailcfg.DERPRegion{}, + 8: &tailcfg.DERPRegion{}, + }, + } + c.derpMap = dm a := c.pickDERPFallback() if a == 0 { t.Fatalf("pickDERPFallback returned 0") @@ -415,7 +426,7 @@ func TestPickDERPFallback(t *testing.T) { got := map[int]int{} for i := 0; i < 50; i++ { c = newConn() - c.derpMap = derpmap.Prod() + c.derpMap = dm got[c.pickDERPFallback()]++ } t.Logf("distribution: %v", got)