|
|
|
|
@ -21,6 +21,7 @@ import ( |
|
|
|
|
|
|
|
|
|
"github.com/tcnksm/go-httpstat" |
|
|
|
|
"inet.af/netaddr" |
|
|
|
|
"tailscale.com/derp/derphttp" |
|
|
|
|
"tailscale.com/net/dnscache" |
|
|
|
|
"tailscale.com/net/interfaces" |
|
|
|
|
"tailscale.com/net/netns" |
|
|
|
|
@ -611,10 +612,15 @@ func newReport() *Report { |
|
|
|
|
//
|
|
|
|
|
// It may not be called concurrently with itself.
|
|
|
|
|
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) { |
|
|
|
|
// Wait for STUN for 3 seconds, but then give HTTP probing
|
|
|
|
|
// another 2 seconds if all UDP failed.
|
|
|
|
|
const overallTimeout = 5 * time.Second |
|
|
|
|
const stunTimeout = 3 * time.Second |
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) |
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, overallTimeout) |
|
|
|
|
defer cancel() |
|
|
|
|
|
|
|
|
|
if dm == nil { |
|
|
|
|
@ -707,7 +713,11 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e |
|
|
|
|
}(probeSet) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
stunTimer := time.NewTimer(stunTimeout) |
|
|
|
|
defer stunTimer.Stop() |
|
|
|
|
|
|
|
|
|
select { |
|
|
|
|
case <-stunTimer.C: |
|
|
|
|
case <-ctx.Done(): |
|
|
|
|
case <-wg.DoneChan(): |
|
|
|
|
case <-rs.stopProbeCh: |
|
|
|
|
@ -719,7 +729,8 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e |
|
|
|
|
rs.stopTimers() |
|
|
|
|
|
|
|
|
|
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
|
|
|
|
if !rs.anyUDP() { |
|
|
|
|
// TODO: this should be moved into the probePlan, using probeProto probeHTTPS.
|
|
|
|
|
if !rs.anyUDP() && ctx.Err() == nil { |
|
|
|
|
var wg sync.WaitGroup |
|
|
|
|
var need []*tailcfg.DERPRegion |
|
|
|
|
for rid, reg := range dm.Regions { |
|
|
|
|
@ -734,11 +745,22 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e |
|
|
|
|
for _, reg := range need { |
|
|
|
|
go func(reg *tailcfg.DERPRegion) { |
|
|
|
|
defer wg.Done() |
|
|
|
|
if d, err := c.measureHTTPSLatency(reg); err != nil { |
|
|
|
|
if d, ip, err := c.measureHTTPSLatency(ctx, reg); err != nil { |
|
|
|
|
c.logf("netcheck: measuring HTTPS latency of %v (%d): %v", reg.RegionCode, reg.RegionID, err) |
|
|
|
|
} else { |
|
|
|
|
rs.mu.Lock() |
|
|
|
|
rs.report.RegionLatency[reg.RegionID] = d |
|
|
|
|
// We set these IPv4 and IPv6 but they're not really used
|
|
|
|
|
// and we don't necessarily set them both. If UDP is blocked
|
|
|
|
|
// and both IPv4 and IPv6 are available over TCP, it's basically
|
|
|
|
|
// random which fields end up getting set here.
|
|
|
|
|
// Since they're not needed, that's fine for now.
|
|
|
|
|
if ip.Is4() { |
|
|
|
|
rs.report.IPv4 = true |
|
|
|
|
} |
|
|
|
|
if ip.Is6() { |
|
|
|
|
rs.report.IPv6 = true |
|
|
|
|
} |
|
|
|
|
rs.mu.Unlock() |
|
|
|
|
} |
|
|
|
|
}(reg) |
|
|
|
|
@ -756,41 +778,64 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e |
|
|
|
|
return report, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: have caller pass in context
|
|
|
|
|
func (c *Client) measureHTTPSLatency(reg *tailcfg.DERPRegion) (time.Duration, error) { |
|
|
|
|
if len(reg.Nodes) == 0 { |
|
|
|
|
return 0, errors.New("no nodes") |
|
|
|
|
} |
|
|
|
|
node := reg.Nodes[0] // TODO: use all nodes per region
|
|
|
|
|
host := node.HostName |
|
|
|
|
// TODO: connect using provided IPv4/IPv6; use a Trasport & set the dialer
|
|
|
|
|
// TODO: also have the caller set the Report.IPv4 or Report.IPv6 bool
|
|
|
|
|
|
|
|
|
|
func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) { |
|
|
|
|
var result httpstat.Result |
|
|
|
|
hctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(context.Background(), &result), 5*time.Second) |
|
|
|
|
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), 5*time.Second) |
|
|
|
|
defer cancel() |
|
|
|
|
|
|
|
|
|
var ip netaddr.IP |
|
|
|
|
|
|
|
|
|
dc := derphttp.NewNetcheckClient(c.logf) |
|
|
|
|
nc, node, err := dc.DialRegion(ctx, reg) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, ip, err |
|
|
|
|
} |
|
|
|
|
defer nc.Close() |
|
|
|
|
|
|
|
|
|
if ta, ok := nc.RemoteAddr().(*net.TCPAddr); ok { |
|
|
|
|
ip, _ = netaddr.FromStdIP(ta.IP) |
|
|
|
|
} |
|
|
|
|
if ip == (netaddr.IP{}) { |
|
|
|
|
return 0, ip, fmt.Errorf("no unexpected RemoteAddr %#v", nc.RemoteAddr()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
connc := make(chan net.Conn, 1) |
|
|
|
|
connc <- nc |
|
|
|
|
|
|
|
|
|
tr := &http.Transport{ |
|
|
|
|
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { |
|
|
|
|
select { |
|
|
|
|
case nc := <-connc: |
|
|
|
|
return nc, nil |
|
|
|
|
default: |
|
|
|
|
return nil, errors.New("only one conn expected") |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
hc := &http.Client{Transport: tr} |
|
|
|
|
|
|
|
|
|
host := dc.TLSServerName(node) |
|
|
|
|
u := fmt.Sprintf("https://%s/derp/latency-check", host) |
|
|
|
|
req, err := http.NewRequestWithContext(hctx, "GET", u, nil) |
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", u, nil) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
return 0, ip, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req) |
|
|
|
|
resp, err := hc.Do(req) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
return 0, ip, err |
|
|
|
|
} |
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
|
_, err = io.Copy(ioutil.Discard, resp.Body) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
return 0, ip, err |
|
|
|
|
} |
|
|
|
|
result.End(c.timeNow()) |
|
|
|
|
|
|
|
|
|
// TODO: decide best timing heuristic here.
|
|
|
|
|
// Maybe the server should return the tcpinfo_rtt?
|
|
|
|
|
return result.ServerProcessing, nil |
|
|
|
|
return result.ServerProcessing, ip, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (c *Client) logConciseReport(r *Report, dm *tailcfg.DERPMap) { |
|
|
|
|
|