mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-16 18:08: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.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.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var netcheckArgs struct {
|
||||
format string
|
||||
every time.Duration
|
||||
format string
|
||||
every time.Duration
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
Logf: logger.WithPrefix(log.Printf, "netcheck: "),
|
||||
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
|
||||
}
|
||||
|
||||
dm := derpmap.Prod()
|
||||
for {
|
||||
t0 := time.Now()
|
||||
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 {
|
||||
log.Fatalf("netcheck: %v", err)
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// name that might be used by Tailscale.
|
||||
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
|
||||
// Tailscale virtual network interfaces.
|
||||
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 }
|
||||
@ -111,10 +81,13 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
// TODO(crawshaw): IPv6 support.
|
||||
// Easy to do here, but we need good endpoint ordering logic.
|
||||
ip := v.IP.To4()
|
||||
if ip == nil {
|
||||
ip, ok := netaddr.FromStdIP(v.IP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip.Is6() {
|
||||
// TODO(crawshaw): IPv6 support.
|
||||
// Easy to do here, but we need good endpoint ordering logic.
|
||||
continue
|
||||
}
|
||||
// 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) }
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -162,7 +135,9 @@ func ForeachInterfaceAddress(fn func(Interface, net.IP)) error {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
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.
|
||||
// For now it's pretty basic.
|
||||
type State struct {
|
||||
InterfaceIPs map[string][]net.IP
|
||||
InterfaceIPs map[string][]netaddr.IP
|
||||
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
|
||||
// considered "expensive", which currently means LTE/etc
|
||||
// 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.
|
||||
func GetState() (*State, error) {
|
||||
s := &State{
|
||||
InterfaceIPs: make(map[string][]net.IP),
|
||||
InterfaceIPs: make(map[string][]netaddr.IP),
|
||||
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.InterfaceUp[ni.Name] = ni.IsUp()
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -227,7 +211,7 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, ip net.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
if isPrivateIP(ip) {
|
||||
if privateIP == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR(s)
|
||||
func isGlobalV6(ip netaddr.IP) bool {
|
||||
return v6Global1.Contains(ip)
|
||||
}
|
||||
|
||||
func mustCIDR(s string) netaddr.IPPrefix {
|
||||
prefix, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ipNet
|
||||
return prefix
|
||||
}
|
||||
|
||||
var (
|
||||
@ -264,4 +252,5 @@ var (
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
cgNAT = mustCIDR("100.64.0.0/10")
|
||||
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.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// Verbose enables verbose logging.
|
||||
Verbose bool
|
||||
|
||||
// Logf optionally specifies where to log to.
|
||||
// If nil, log.Printf is used.
|
||||
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
|
||||
// probe packet that we sent to ourselves.
|
||||
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
|
||||
// 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 {
|
||||
return makeProbePlanInitial(dm, have6if)
|
||||
return makeProbePlanInitial(dm, ifState)
|
||||
}
|
||||
have6if := ifState.HaveV6Global
|
||||
have4if := ifState.HaveV4
|
||||
plan = make(probePlan)
|
||||
if !have4if && !have6if {
|
||||
return plan
|
||||
}
|
||||
had4 := len(last.RegionV4Latency) > 0
|
||||
had6 := len(last.RegionV6Latency) > 0
|
||||
hadBoth := have6if && had4 && had6
|
||||
@ -263,7 +277,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, have6if bool, last *Report) (plan probeP
|
||||
break
|
||||
}
|
||||
var p4, p6 []probe
|
||||
do4 := true
|
||||
do4 := have4if
|
||||
do6 := have6if
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func makeProbePlanInitial(dm *tailcfg.DERPMap, have6if bool) (plan probePlan) {
|
||||
func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *interfaces.State) (plan probePlan) {
|
||||
plan = make(probePlan)
|
||||
|
||||
// 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++ {
|
||||
n := reg.Nodes[try%len(reg.Nodes)]
|
||||
delay := time.Duration(try) * initialSTUNTimeout
|
||||
if nodeMight4(n) {
|
||||
if ifState.HaveV4 && nodeMight4(n) {
|
||||
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})
|
||||
}
|
||||
}
|
||||
@ -416,12 +430,15 @@ type reportState struct {
|
||||
pc4 STUNConn
|
||||
pc6 STUNConn
|
||||
pc4Hair net.PacketConn
|
||||
incremental bool // doing a lite, follow-up netcheck
|
||||
stopProbeCh chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
sentHairCheck bool
|
||||
report *Report // to be returned by GetReport
|
||||
inFlight map[stun.TxID]func(netaddr.IPPort) // called without c.mu held
|
||||
gotEP4 string
|
||||
timers []*time.Timer
|
||||
}
|
||||
|
||||
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) {
|
||||
if rs.sentHairCheck {
|
||||
if rs.sentHairCheck || rs.incremental {
|
||||
return
|
||||
}
|
||||
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) })
|
||||
}
|
||||
|
||||
func (rs *reportState) waitHairCheck(ctx context.Context) {
|
||||
rs.mu.Lock()
|
||||
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 {
|
||||
return
|
||||
}
|
||||
ret := rs.report
|
||||
|
||||
select {
|
||||
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
|
||||
// is non-zero (for all but HTTPS replies), it's recorded as our UDP
|
||||
// IP:port.
|
||||
@ -520,6 +553,19 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
||||
ret.UDP = true
|
||||
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 {
|
||||
case ipp.IP.Is6():
|
||||
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 {
|
||||
return &Report{
|
||||
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
|
||||
gotHairSTUN: make(chan *net.UDPAddr, 1),
|
||||
hairTimeout: make(chan struct{}),
|
||||
stopProbeCh: make(chan struct{}, 1),
|
||||
}
|
||||
c.curState = rs
|
||||
last := c.last
|
||||
@ -586,6 +640,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
c.nextFull = false
|
||||
c.lastFull = now
|
||||
}
|
||||
rs.incremental = last != nil
|
||||
c.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
@ -594,9 +649,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
c.curState = nil
|
||||
}()
|
||||
|
||||
v6iface, err := interfaces.HaveIPv6GlobalAddress()
|
||||
ifState, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
c.logf("interfaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if v6iface {
|
||||
if ifState.HaveV6Global {
|
||||
if f := c.GetSTUNConn6; f != nil {
|
||||
rs.pc6 = f()
|
||||
} 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.Add(len(plan))
|
||||
@ -651,9 +707,13 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-wg.DoneChan():
|
||||
case <-rs.stopProbeCh:
|
||||
// Saw enough regions.
|
||||
c.vlogf("saw enough regions; not waiting for rest")
|
||||
}
|
||||
|
||||
rs.waitHairCheck(ctx)
|
||||
rs.stopTimers()
|
||||
|
||||
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
||||
if !rs.anyUDP() {
|
||||
@ -882,6 +942,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
|
||||
default:
|
||||
panic("bad probe proto " + fmt.Sprint(probe.proto))
|
||||
}
|
||||
c.vlogf("sent to %v", addr)
|
||||
}
|
||||
|
||||
// proto is 4 or 6
|
||||
@ -933,3 +994,12 @@ func regionHasDERPNode(r *tailcfg.DERPRegion) bool {
|
||||
}
|
||||
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"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/net/stun/stuntest"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -256,6 +257,7 @@ func TestMakeProbePlan(t *testing.T) {
|
||||
name string
|
||||
dm *tailcfg.DERPMap
|
||||
have6if bool
|
||||
no4 bool // no IPv4
|
||||
last *Report
|
||||
want probePlan
|
||||
}{
|
||||
@ -371,10 +373,27 @@ func TestMakeProbePlan(t *testing.T) {
|
||||
"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 {
|
||||
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) {
|
||||
t.Errorf("unexpected plan; got:\n%v\nwant:\n%v\n", got, tt.want)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user