mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-19 19:38:40 +00:00
Make netcheck handle v6-only interfaces better, faster.
Also: * add -verbose flag to cmd/tailscale netcheck * remove some API from the interfaces package * convert some of the interfaces package to netaddr.IP * don't even send IPv4 probes on machines with no IPv4 (or only v4 loopback) * and once three regions have replied, stop waiting for other probes at 2x the slowest duration. Updates #376
This commit is contained in:
parent
c5495288a6
commit
0245bbe97b
@ -31,27 +31,36 @@ var netcheckCmd = &ffcli.Command{
|
|||||||
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
|
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
|
||||||
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
|
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
|
||||||
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
|
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
|
||||||
|
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
|
||||||
return fs
|
return fs
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var netcheckArgs struct {
|
var netcheckArgs struct {
|
||||||
format string
|
format string
|
||||||
every time.Duration
|
every time.Duration
|
||||||
|
verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runNetcheck(ctx context.Context, args []string) error {
|
func runNetcheck(ctx context.Context, args []string) error {
|
||||||
c := &netcheck.Client{
|
c := &netcheck.Client{
|
||||||
Logf: logger.WithPrefix(log.Printf, "netcheck: "),
|
|
||||||
DNSCache: dnscache.Get(),
|
DNSCache: dnscache.Get(),
|
||||||
}
|
}
|
||||||
if netcheckArgs.every != 0 {
|
if netcheckArgs.verbose {
|
||||||
|
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||||
|
c.Verbose = true
|
||||||
|
} else {
|
||||||
c.Logf = logger.Discard
|
c.Logf = logger.Discard
|
||||||
}
|
}
|
||||||
|
|
||||||
dm := derpmap.Prod()
|
dm := derpmap.Prod()
|
||||||
for {
|
for {
|
||||||
|
t0 := time.Now()
|
||||||
report, err := c.GetReport(ctx, dm)
|
report, err := c.GetReport(ctx, dm)
|
||||||
|
d := time.Since(t0)
|
||||||
|
if netcheckArgs.verbose {
|
||||||
|
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("netcheck: %v", err)
|
log.Fatalf("netcheck: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tailscale returns the current machine's Tailscale interface, if any.
|
// Tailscale returns the current machine's Tailscale interface, if any.
|
||||||
@ -37,39 +39,6 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
|||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HaveIPv6GlobalAddress reports whether the machine appears to have a
|
|
||||||
// global scope unicast IPv6 address.
|
|
||||||
//
|
|
||||||
// It only returns an error if there's a problem querying the system
|
|
||||||
// interfaces.
|
|
||||||
func HaveIPv6GlobalAddress() (bool, error) {
|
|
||||||
ifs, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for i := range ifs {
|
|
||||||
iface := &ifs[i]
|
|
||||||
if !isUp(iface) || isLoopback(iface) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, a := range addrs {
|
|
||||||
ipnet, ok := a.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ipnet.IP.To4() != nil || !ipnet.IP.IsGlobalUnicast() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybeTailscaleInterfaceName reports whether s is an interface
|
// maybeTailscaleInterfaceName reports whether s is an interface
|
||||||
// name that might be used by Tailscale.
|
// name that might be used by Tailscale.
|
||||||
func maybeTailscaleInterfaceName(s string) bool {
|
func maybeTailscaleInterfaceName(s string) bool {
|
||||||
@ -82,7 +51,8 @@ func maybeTailscaleInterfaceName(s string) bool {
|
|||||||
// IsTailscaleIP reports whether ip is an IP in a range used by
|
// IsTailscaleIP reports whether ip is an IP in a range used by
|
||||||
// Tailscale virtual network interfaces.
|
// Tailscale virtual network interfaces.
|
||||||
func IsTailscaleIP(ip net.IP) bool {
|
func IsTailscaleIP(ip net.IP) bool {
|
||||||
return cgNAT.Contains(ip)
|
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
||||||
|
return cgNAT.Contains(nip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||||
@ -111,10 +81,13 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
|||||||
for _, a := range addrs {
|
for _, a := range addrs {
|
||||||
switch v := a.(type) {
|
switch v := a.(type) {
|
||||||
case *net.IPNet:
|
case *net.IPNet:
|
||||||
// TODO(crawshaw): IPv6 support.
|
ip, ok := netaddr.FromStdIP(v.IP)
|
||||||
// Easy to do here, but we need good endpoint ordering logic.
|
if !ok {
|
||||||
ip := v.IP.To4()
|
continue
|
||||||
if ip == nil {
|
}
|
||||||
|
if ip.Is6() {
|
||||||
|
// TODO(crawshaw): IPv6 support.
|
||||||
|
// Easy to do here, but we need good endpoint ordering logic.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// TODO(apenwarr): don't special case cgNAT.
|
// TODO(apenwarr): don't special case cgNAT.
|
||||||
@ -148,7 +121,7 @@ func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
|||||||
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
||||||
|
|
||||||
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
||||||
func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -162,7 +135,9 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
|||||||
for _, a := range addrs {
|
for _, a := range addrs {
|
||||||
switch v := a.(type) {
|
switch v := a.(type) {
|
||||||
case *net.IPNet:
|
case *net.IPNet:
|
||||||
fn(Interface{iface}, v.IP)
|
if ip, ok := netaddr.FromStdIP(v.IP); ok {
|
||||||
|
fn(Interface{iface}, ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,9 +148,16 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
|||||||
// routing table, and other network configuration.
|
// routing table, and other network configuration.
|
||||||
// For now it's pretty basic.
|
// For now it's pretty basic.
|
||||||
type State struct {
|
type State struct {
|
||||||
InterfaceIPs map[string][]net.IP
|
InterfaceIPs map[string][]netaddr.IP
|
||||||
InterfaceUp map[string]bool
|
InterfaceUp map[string]bool
|
||||||
|
|
||||||
|
// HaveV6Global is whether this machine has an IPv6 global address
|
||||||
|
// on some interface.
|
||||||
|
HaveV6Global bool
|
||||||
|
|
||||||
|
// HaveV4 is whether the machine has some non-localhost IPv4 address.
|
||||||
|
HaveV4 bool
|
||||||
|
|
||||||
// IsExpensive is whether the current network interface is
|
// IsExpensive is whether the current network interface is
|
||||||
// considered "expensive", which currently means LTE/etc
|
// considered "expensive", which currently means LTE/etc
|
||||||
// instead of Wifi. This field is not populated by GetState.
|
// instead of Wifi. This field is not populated by GetState.
|
||||||
@ -204,12 +186,14 @@ func (s *State) RemoveTailscaleInterfaces() {
|
|||||||
// It does not set the returned State.IsExpensive. The caller can populate that.
|
// It does not set the returned State.IsExpensive. The caller can populate that.
|
||||||
func GetState() (*State, error) {
|
func GetState() (*State, error) {
|
||||||
s := &State{
|
s := &State{
|
||||||
InterfaceIPs: make(map[string][]net.IP),
|
InterfaceIPs: make(map[string][]netaddr.IP),
|
||||||
InterfaceUp: make(map[string]bool),
|
InterfaceUp: make(map[string]bool),
|
||||||
}
|
}
|
||||||
if err := ForeachInterfaceAddress(func(ni Interface, ip net.IP) {
|
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
|
||||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
||||||
s.InterfaceUp[ni.Name] = ni.IsUp()
|
s.InterfaceUp[ni.Name] = ni.IsUp()
|
||||||
|
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||||
|
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -227,7 +211,7 @@ func HTTPOfListener(ln net.Listener) string {
|
|||||||
|
|
||||||
var goodIP string
|
var goodIP string
|
||||||
var privateIP string
|
var privateIP string
|
||||||
ForeachInterfaceAddress(func(i Interface, ip net.IP) {
|
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||||
if isPrivateIP(ip) {
|
if isPrivateIP(ip) {
|
||||||
if privateIP == "" {
|
if privateIP == "" {
|
||||||
privateIP = ip.String()
|
privateIP = ip.String()
|
||||||
@ -246,16 +230,20 @@ func HTTPOfListener(ln net.Listener) string {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPrivateIP(ip net.IP) bool {
|
func isPrivateIP(ip netaddr.IP) bool {
|
||||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustCIDR(s string) *net.IPNet {
|
func isGlobalV6(ip netaddr.IP) bool {
|
||||||
_, ipNet, err := net.ParseCIDR(s)
|
return v6Global1.Contains(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCIDR(s string) netaddr.IPPrefix {
|
||||||
|
prefix, err := netaddr.ParseIPPrefix(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return ipNet
|
return prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -264,4 +252,5 @@ var (
|
|||||||
private3 = mustCIDR("192.168.0.0/16")
|
private3 = mustCIDR("192.168.0.0/16")
|
||||||
cgNAT = mustCIDR("100.64.0.0/10")
|
cgNAT = mustCIDR("100.64.0.0/10")
|
||||||
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
||||||
|
v6Global1 = mustCIDR("2000::/3")
|
||||||
)
|
)
|
||||||
|
@ -74,6 +74,9 @@ type Client struct {
|
|||||||
// If nil, a DNS cache is not used.
|
// If nil, a DNS cache is not used.
|
||||||
DNSCache *dnscache.Resolver
|
DNSCache *dnscache.Resolver
|
||||||
|
|
||||||
|
// Verbose enables verbose logging.
|
||||||
|
Verbose bool
|
||||||
|
|
||||||
// Logf optionally specifies where to log to.
|
// Logf optionally specifies where to log to.
|
||||||
// If nil, log.Printf is used.
|
// If nil, log.Printf is used.
|
||||||
Logf logger.Logf
|
Logf logger.Logf
|
||||||
@ -112,6 +115,12 @@ func (c *Client) logf(format string, a ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) vlogf(format string, a ...interface{}) {
|
||||||
|
if c.Verbose {
|
||||||
|
c.logf(format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleHairSTUN reports whether pkt (from src) was our magic hairpin
|
// handleHairSTUN reports whether pkt (from src) was our magic hairpin
|
||||||
// probe packet that we sent to ourselves.
|
// probe packet that we sent to ourselves.
|
||||||
func (c *Client) handleHairSTUNLocked(pkt []byte, src *net.UDPAddr) bool {
|
func (c *Client) handleHairSTUNLocked(pkt []byte, src *net.UDPAddr) bool {
|
||||||
@ -250,11 +259,16 @@ const numIncrementalRegions = 3
|
|||||||
|
|
||||||
// makeProbePlan generates the probe plan for a DERPMap, given the most
|
// makeProbePlan generates the probe plan for a DERPMap, given the most
|
||||||
// recent report and whether IPv6 is configured on an interface.
|
// recent report and whether IPv6 is configured on an interface.
|
||||||
func makeProbePlan(dm *tailcfg.DERPMap, have6if bool, last *Report) (plan probePlan) {
|
func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report) (plan probePlan) {
|
||||||
if last == nil || len(last.RegionLatency) == 0 {
|
if last == nil || len(last.RegionLatency) == 0 {
|
||||||
return makeProbePlanInitial(dm, have6if)
|
return makeProbePlanInitial(dm, ifState)
|
||||||
}
|
}
|
||||||
|
have6if := ifState.HaveV6Global
|
||||||
|
have4if := ifState.HaveV4
|
||||||
plan = make(probePlan)
|
plan = make(probePlan)
|
||||||
|
if !have4if && !have6if {
|
||||||
|
return plan
|
||||||
|
}
|
||||||
had4 := len(last.RegionV4Latency) > 0
|
had4 := len(last.RegionV4Latency) > 0
|
||||||
had6 := len(last.RegionV6Latency) > 0
|
had6 := len(last.RegionV6Latency) > 0
|
||||||
hadBoth := have6if && had4 && had6
|
hadBoth := have6if && had4 && had6
|
||||||
@ -263,7 +277,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, have6if bool, last *Report) (plan probeP
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
var p4, p6 []probe
|
var p4, p6 []probe
|
||||||
do4 := true
|
do4 := have4if
|
||||||
do6 := have6if
|
do6 := have6if
|
||||||
|
|
||||||
// By default, each node only gets one STUN packet sent,
|
// By default, each node only gets one STUN packet sent,
|
||||||
@ -317,7 +331,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, have6if bool, last *Report) (plan probeP
|
|||||||
return plan
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeProbePlanInitial(dm *tailcfg.DERPMap, have6if bool) (plan probePlan) {
|
func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *interfaces.State) (plan probePlan) {
|
||||||
plan = make(probePlan)
|
plan = make(probePlan)
|
||||||
|
|
||||||
// initialSTUNTimeout is only 100ms because some extra retransmits
|
// initialSTUNTimeout is only 100ms because some extra retransmits
|
||||||
@ -330,10 +344,10 @@ func makeProbePlanInitial(dm *tailcfg.DERPMap, have6if bool) (plan probePlan) {
|
|||||||
for try := 0; try < 3; try++ {
|
for try := 0; try < 3; try++ {
|
||||||
n := reg.Nodes[try%len(reg.Nodes)]
|
n := reg.Nodes[try%len(reg.Nodes)]
|
||||||
delay := time.Duration(try) * initialSTUNTimeout
|
delay := time.Duration(try) * initialSTUNTimeout
|
||||||
if nodeMight4(n) {
|
if ifState.HaveV4 && nodeMight4(n) {
|
||||||
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
|
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
|
||||||
}
|
}
|
||||||
if have6if && nodeMight6(n) {
|
if ifState.HaveV6Global && nodeMight6(n) {
|
||||||
p6 = append(p6, probe{delay: delay, node: n.Name, proto: probeIPv6})
|
p6 = append(p6, probe{delay: delay, node: n.Name, proto: probeIPv6})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,12 +430,15 @@ type reportState struct {
|
|||||||
pc4 STUNConn
|
pc4 STUNConn
|
||||||
pc6 STUNConn
|
pc6 STUNConn
|
||||||
pc4Hair net.PacketConn
|
pc4Hair net.PacketConn
|
||||||
|
incremental bool // doing a lite, follow-up netcheck
|
||||||
|
stopProbeCh chan struct{}
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sentHairCheck bool
|
sentHairCheck bool
|
||||||
report *Report // to be returned by GetReport
|
report *Report // to be returned by GetReport
|
||||||
inFlight map[stun.TxID]func(netaddr.IPPort) // called without c.mu held
|
inFlight map[stun.TxID]func(netaddr.IPPort) // called without c.mu held
|
||||||
gotEP4 string
|
gotEP4 string
|
||||||
|
timers []*time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *reportState) anyUDP() bool {
|
func (rs *reportState) anyUDP() bool {
|
||||||
@ -472,21 +489,29 @@ func (rs *reportState) probeWouldHelp(probe probe, node *tailcfg.DERPNode) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *reportState) startHairCheckLocked(dst netaddr.IPPort) {
|
func (rs *reportState) startHairCheckLocked(dst netaddr.IPPort) {
|
||||||
if rs.sentHairCheck {
|
if rs.sentHairCheck || rs.incremental {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rs.sentHairCheck = true
|
rs.sentHairCheck = true
|
||||||
rs.pc4Hair.WriteTo(stun.Request(rs.hairTX), dst.UDPAddr())
|
ua := dst.UDPAddr()
|
||||||
|
rs.pc4Hair.WriteTo(stun.Request(rs.hairTX), ua)
|
||||||
|
rs.c.vlogf("sent haircheck to %v", ua)
|
||||||
time.AfterFunc(500*time.Millisecond, func() { close(rs.hairTimeout) })
|
time.AfterFunc(500*time.Millisecond, func() { close(rs.hairTimeout) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *reportState) waitHairCheck(ctx context.Context) {
|
func (rs *reportState) waitHairCheck(ctx context.Context) {
|
||||||
rs.mu.Lock()
|
rs.mu.Lock()
|
||||||
defer rs.mu.Unlock()
|
defer rs.mu.Unlock()
|
||||||
|
ret := rs.report
|
||||||
|
if rs.incremental {
|
||||||
|
if rs.c.last != nil {
|
||||||
|
ret.HairPinning = rs.c.last.HairPinning
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if !rs.sentHairCheck {
|
if !rs.sentHairCheck {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ret := rs.report
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-rs.gotHairSTUN:
|
case <-rs.gotHairSTUN:
|
||||||
@ -504,6 +529,14 @@ func (rs *reportState) waitHairCheck(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *reportState) stopTimers() {
|
||||||
|
rs.mu.Lock()
|
||||||
|
defer rs.mu.Unlock()
|
||||||
|
for _, t := range rs.timers {
|
||||||
|
t.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// addNodeLatency updates rs to note that node's latency is d. If ipp
|
// addNodeLatency updates rs to note that node's latency is d. If ipp
|
||||||
// is non-zero (for all but HTTPS replies), it's recorded as our UDP
|
// is non-zero (for all but HTTPS replies), it's recorded as our UDP
|
||||||
// IP:port.
|
// IP:port.
|
||||||
@ -520,6 +553,19 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
|||||||
ret.UDP = true
|
ret.UDP = true
|
||||||
updateLatency(ret.RegionLatency, node.RegionID, d)
|
updateLatency(ret.RegionLatency, node.RegionID, d)
|
||||||
|
|
||||||
|
// Once we've heard from 3 regions, start a timer to give up
|
||||||
|
// on the other ones. The timer's duration is a function of
|
||||||
|
// whether this is our initial full probe or an incremental
|
||||||
|
// one. For incremental ones, wait for the duration of the
|
||||||
|
// slowest region. For initial ones, double that.
|
||||||
|
if len(ret.RegionLatency) == 3 {
|
||||||
|
timeout := maxDurationValue(ret.RegionLatency)
|
||||||
|
if !rs.incremental {
|
||||||
|
timeout *= 2
|
||||||
|
}
|
||||||
|
rs.timers = append(rs.timers, time.AfterFunc(timeout, rs.stopProbes))
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ipp.IP.Is6():
|
case ipp.IP.Is6():
|
||||||
updateLatency(ret.RegionV6Latency, node.RegionID, d)
|
updateLatency(ret.RegionV6Latency, node.RegionID, d)
|
||||||
@ -543,6 +589,13 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *reportState) stopProbes() {
|
||||||
|
select {
|
||||||
|
case rs.stopProbeCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newReport() *Report {
|
func newReport() *Report {
|
||||||
return &Report{
|
return &Report{
|
||||||
RegionLatency: make(map[int]time.Duration),
|
RegionLatency: make(map[int]time.Duration),
|
||||||
@ -577,6 +630,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
hairTX: stun.NewTxID(), // random payload
|
hairTX: stun.NewTxID(), // random payload
|
||||||
gotHairSTUN: make(chan *net.UDPAddr, 1),
|
gotHairSTUN: make(chan *net.UDPAddr, 1),
|
||||||
hairTimeout: make(chan struct{}),
|
hairTimeout: make(chan struct{}),
|
||||||
|
stopProbeCh: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
c.curState = rs
|
c.curState = rs
|
||||||
last := c.last
|
last := c.last
|
||||||
@ -586,6 +640,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
c.nextFull = false
|
c.nextFull = false
|
||||||
c.lastFull = now
|
c.lastFull = now
|
||||||
}
|
}
|
||||||
|
rs.incremental = last != nil
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -594,9 +649,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
c.curState = nil
|
c.curState = nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
v6iface, err := interfaces.HaveIPv6GlobalAddress()
|
ifState, err := interfaces.GetState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logf("interfaces: %v", err)
|
c.logf("interfaces: %v", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a UDP4 socket used for sending to our discovered IPv4 address.
|
// Create a UDP4 socket used for sending to our discovered IPv4 address.
|
||||||
@ -619,7 +675,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
go c.readPackets(ctx, u4)
|
go c.readPackets(ctx, u4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v6iface {
|
if ifState.HaveV6Global {
|
||||||
if f := c.GetSTUNConn6; f != nil {
|
if f := c.GetSTUNConn6; f != nil {
|
||||||
rs.pc6 = f()
|
rs.pc6 = f()
|
||||||
} else {
|
} else {
|
||||||
@ -633,7 +689,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plan := makeProbePlan(dm, v6iface, last)
|
plan := makeProbePlan(dm, ifState, last)
|
||||||
|
|
||||||
wg := syncs.NewWaitGroupChan()
|
wg := syncs.NewWaitGroupChan()
|
||||||
wg.Add(len(plan))
|
wg.Add(len(plan))
|
||||||
@ -651,9 +707,13 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
case <-wg.DoneChan():
|
case <-wg.DoneChan():
|
||||||
|
case <-rs.stopProbeCh:
|
||||||
|
// Saw enough regions.
|
||||||
|
c.vlogf("saw enough regions; not waiting for rest")
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.waitHairCheck(ctx)
|
rs.waitHairCheck(ctx)
|
||||||
|
rs.stopTimers()
|
||||||
|
|
||||||
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
||||||
if !rs.anyUDP() {
|
if !rs.anyUDP() {
|
||||||
@ -882,6 +942,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
|
|||||||
default:
|
default:
|
||||||
panic("bad probe proto " + fmt.Sprint(probe.proto))
|
panic("bad probe proto " + fmt.Sprint(probe.proto))
|
||||||
}
|
}
|
||||||
|
c.vlogf("sent to %v", addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// proto is 4 or 6
|
// proto is 4 or 6
|
||||||
@ -933,3 +994,12 @@ func regionHasDERPNode(r *tailcfg.DERPRegion) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maxDurationValue(m map[int]time.Duration) (max time.Duration) {
|
||||||
|
for _, v := range m {
|
||||||
|
if v > max {
|
||||||
|
max = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
"tailscale.com/net/stun"
|
"tailscale.com/net/stun"
|
||||||
"tailscale.com/net/stun/stuntest"
|
"tailscale.com/net/stun/stuntest"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@ -256,6 +257,7 @@ func TestMakeProbePlan(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
dm *tailcfg.DERPMap
|
dm *tailcfg.DERPMap
|
||||||
have6if bool
|
have6if bool
|
||||||
|
no4 bool // no IPv4
|
||||||
last *Report
|
last *Report
|
||||||
want probePlan
|
want probePlan
|
||||||
}{
|
}{
|
||||||
@ -371,10 +373,27 @@ func TestMakeProbePlan(t *testing.T) {
|
|||||||
"region-3-v4": []probe{p("3a", 4)},
|
"region-3-v4": []probe{p("3a", 4)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "only_v6_initial",
|
||||||
|
have6if: true,
|
||||||
|
no4: true,
|
||||||
|
dm: basicMap,
|
||||||
|
want: probePlan{
|
||||||
|
"region-1-v6": []probe{p("1a", 6), p("1a", 6, 100*ms), p("1a", 6, 200*ms)},
|
||||||
|
"region-2-v6": []probe{p("2a", 6), p("2b", 6, 100*ms), p("2a", 6, 200*ms)},
|
||||||
|
"region-3-v6": []probe{p("3a", 6), p("3b", 6, 100*ms), p("3c", 6, 200*ms)},
|
||||||
|
"region-4-v6": []probe{p("4a", 6), p("4b", 6, 100*ms), p("4c", 6, 200*ms)},
|
||||||
|
"region-5-v6": []probe{p("5a", 6), p("5b", 6, 100*ms), p("5c", 6, 200*ms)},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := makeProbePlan(tt.dm, tt.have6if, tt.last)
|
ifState := &interfaces.State{
|
||||||
|
HaveV6Global: tt.have6if,
|
||||||
|
HaveV4: !tt.no4,
|
||||||
|
}
|
||||||
|
got := makeProbePlan(tt.dm, ifState, tt.last)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("unexpected plan; got:\n%v\nwant:\n%v\n", got, tt.want)
|
t.Errorf("unexpected plan; got:\n%v\nwant:\n%v\n", got, tt.want)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user