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 <bradfitz@tailscale.com>
pull/3202/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
parent eebe7afad7
commit b0b0a80318

@ -176,6 +176,7 @@ func main() {
derpHandler := derphttp.Handler(s) derpHandler := derphttp.Handler(s)
derpHandler = addWebSocketSupport(s, derpHandler) derpHandler = addWebSocketSupport(s, derpHandler)
mux.Handle("/derp", derpHandler) mux.Handle("/derp", derpHandler)
mux.HandleFunc("/derp/probe", probeHandler)
go refreshBootstrapDNSLoop() go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS) mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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")) pc, err := net.ListenPacket("udp", net.JoinHostPort(host, "3478"))
if err != nil { if err != nil {
log.Fatalf("failed to open STUN listener: %v", err) log.Fatalf("failed to open STUN listener: %v", err)

@ -738,16 +738,6 @@ func (c *Client) udpBindAddr() string {
// //
// It may not be called concurrently with itself. // It may not be called concurrently with itself.
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) { 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 // Mask user context with ours that we guarantee to cancel so
// we can depend on it being closed in goroutines later. // we can depend on it being closed in goroutines later.
// (User ctx might be context.Background, etc) // (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 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() ifState, err := interfaces.GetState()
if err != nil { if err != nil {
c.logf("[v1] interfaces: %v", err) c.logf("[v1] interfaces: %v", err)
@ -922,6 +919,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
wg.Wait() wg.Wait()
} }
return c.finishAndStoreReport(rs, dm), nil
}
func (c *Client) finishAndStoreReport(rs *reportState, dm *tailcfg.DERPMap) *Report {
rs.mu.Lock() rs.mu.Lock()
report := rs.report.Clone() report := rs.report.Clone()
rs.mu.Unlock() rs.mu.Unlock()
@ -929,7 +930,56 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
c.addReportHistoryAndSetPreferredDERP(report) c.addReportHistoryAndSetPreferredDERP(report)
c.logConciseReport(report, dm) 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) { func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) {

@ -3578,6 +3578,9 @@ func (de *endpoint) sendPingsLocked(now mono.Time, sendCallMeMaybe bool) {
de.deleteEndpointLocked(ep) de.deleteEndpointLocked(ep)
continue continue
} }
if runtime.GOOS == "js" {
continue
}
if !st.lastPing.IsZero() && now.Sub(st.lastPing) < discoPingInterval { if !st.lastPing.IsZero() && now.Sub(st.lastPing) < discoPingInterval {
continue continue
} }

Loading…
Cancel
Save