From b0b0a8031864d7baa01b2553e5c85c35a73cf3c5 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 27 Oct 2021 09:37:32 -0700 Subject: [PATCH] net/netcheck: implement netcheck for js/wasm clients And the derper change to add a CORS endpoint for latency measurement. And a little magicsock change to cut down some log spam on js/wasm. Updates #3157 Change-Id: I5fd9e6f5098c815116ddc8ac90cbcd0602098a48 Signed-off-by: Brad Fitzpatrick --- cmd/derper/derper.go | 13 +++++- net/netcheck/netcheck.go | 72 ++++++++++++++++++++++++++++----- wgengine/magicsock/magicsock.go | 3 ++ 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/cmd/derper/derper.go b/cmd/derper/derper.go index 4094e5a36..7fcaf5923 100644 --- a/cmd/derper/derper.go +++ b/cmd/derper/derper.go @@ -176,6 +176,7 @@ func main() { derpHandler := derphttp.Handler(s) derpHandler = addWebSocketSupport(s, derpHandler) mux.Handle("/derp", derpHandler) + mux.HandleFunc("/derp/probe", probeHandler) go refreshBootstrapDNSLoop() mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -271,8 +272,18 @@ func main() { } } -func serveSTUN(host string) { +// probeHandler is the endpoint that js/wasm clients hit to measure +// DERP latency, since they can't do UDP STUN queries. +func probeHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "HEAD", "GET": + w.Header().Set("Access-Control-Allow-Origin", "*") + default: + http.Error(w, "bogus probe method", http.StatusMethodNotAllowed) + } +} +func serveSTUN(host string) { pc, err := net.ListenPacket("udp", net.JoinHostPort(host, "3478")) if err != nil { log.Fatalf("failed to open STUN listener: %v", err) diff --git a/net/netcheck/netcheck.go b/net/netcheck/netcheck.go index fc2f8ee12..e20a68d3a 100644 --- a/net/netcheck/netcheck.go +++ b/net/netcheck/netcheck.go @@ -738,16 +738,6 @@ func (c *Client) udpBindAddr() string { // // It may not be called concurrently with itself. func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) { - if runtime.GOOS == "js" { - // TODO(bradfitz): do a real js/wasm netcheck, once - // the WebSocket-capable DERPs are rolled out. For now - // you need to be running in a tailnet with Region 900 - // derper available that supports webassembly. - return &Report{ - PreferredDERP: 900, - }, nil - } - // Mask user context with ours that we guarantee to cancel so // we can depend on it being closed in goroutines later. // (User ctx might be context.Background, etc) @@ -789,6 +779,13 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e c.curState = nil }() + if runtime.GOOS == "js" { + if err := c.runHTTPOnlyChecks(ctx, last, rs, dm); err != nil { + return nil, err + } + return c.finishAndStoreReport(rs, dm), nil + } + ifState, err := interfaces.GetState() if err != nil { c.logf("[v1] interfaces: %v", err) @@ -922,6 +919,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e wg.Wait() } + return c.finishAndStoreReport(rs, dm), nil +} + +func (c *Client) finishAndStoreReport(rs *reportState, dm *tailcfg.DERPMap) *Report { rs.mu.Lock() report := rs.report.Clone() rs.mu.Unlock() @@ -929,7 +930,56 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e c.addReportHistoryAndSetPreferredDERP(report) c.logConciseReport(report, dm) - return report, nil + return report +} + +// runHTTPOnlyChecks is the netcheck done by environments that can +// only do HTTP requests, such as ws/wasm. +func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *reportState, dm *tailcfg.DERPMap) error { + var regions []*tailcfg.DERPRegion + if rs.incremental && last != nil { + for rid := range last.RegionLatency { + if dr, ok := dm.Regions[rid]; ok { + regions = append(regions, dr) + } + } + } + if len(regions) == 0 { + for _, dr := range dm.Regions { + regions = append(regions, dr) + } + } + c.logf("running HTTP-only netcheck against %v regions", len(regions)) + + var wg sync.WaitGroup + for _, rg := range regions { + if len(rg.Nodes) == 0 { + continue + } + wg.Add(1) + rg := rg + go func() { + defer wg.Done() + node := rg.Nodes[0] + req, _ := http.NewRequestWithContext(ctx, "HEAD", "https://"+node.HostName+"/derp/probe", nil) + // One warm-up one to get HTTP connection set + // up and get a connection from the browser's + // pool. + if _, err := http.DefaultClient.Do(req); err != nil { + c.logf("probing %s: %v", node.HostName, err) + return + } + t0 := c.timeNow() + if _, err := http.DefaultClient.Do(req); err != nil { + c.logf("probing %s: %v", node.HostName, err) + return + } + d := c.timeNow().Sub(t0) + rs.addNodeLatency(node, netaddr.IPPort{}, d) + }() + } + wg.Wait() + return nil } func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) { diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 440736bcc..025395458 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -3578,6 +3578,9 @@ func (de *endpoint) sendPingsLocked(now mono.Time, sendCallMeMaybe bool) { de.deleteEndpointLocked(ep) continue } + if runtime.GOOS == "js" { + continue + } if !st.lastPing.IsZero() && now.Sub(st.lastPing) < discoPingInterval { continue }