|
|
@ -15,6 +15,7 @@ import (
|
|
|
|
"html"
|
|
|
|
"html"
|
|
|
|
"io"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"log"
|
|
|
|
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
"sort"
|
|
|
|
"sync"
|
|
|
|
"sync"
|
|
|
@ -22,6 +23,7 @@ import (
|
|
|
|
|
|
|
|
|
|
|
|
"tailscale.com/derp"
|
|
|
|
"tailscale.com/derp"
|
|
|
|
"tailscale.com/derp/derphttp"
|
|
|
|
"tailscale.com/derp/derphttp"
|
|
|
|
|
|
|
|
"tailscale.com/net/stun"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/types/key"
|
|
|
|
"tailscale.com/types/key"
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -67,22 +69,27 @@ func getOverallStatus() (o overallStatus) {
|
|
|
|
if age := now.Sub(lastDERPMapAt); age > time.Minute {
|
|
|
|
if age := now.Sub(lastDERPMapAt); age > time.Minute {
|
|
|
|
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
|
|
|
|
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addPairMeta := func(pair nodePair) {
|
|
|
|
|
|
|
|
st, ok := state[pair]
|
|
|
|
|
|
|
|
age := now.Sub(st.at).Round(time.Second)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
|
|
|
case !ok:
|
|
|
|
|
|
|
|
o.addBadf("no state for %v", pair)
|
|
|
|
|
|
|
|
case st.err != nil:
|
|
|
|
|
|
|
|
o.addBadf("%v: %v", pair, st.err)
|
|
|
|
|
|
|
|
case age > 90*time.Second:
|
|
|
|
|
|
|
|
o.addBadf("%v: update is %v old", pair, age)
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, reg := range sortedRegions(lastDERPMap) {
|
|
|
|
for _, reg := range sortedRegions(lastDERPMap) {
|
|
|
|
for _, from := range reg.Nodes {
|
|
|
|
for _, from := range reg.Nodes {
|
|
|
|
|
|
|
|
addPairMeta(nodePair{"UDP", from.Name})
|
|
|
|
for _, to := range reg.Nodes {
|
|
|
|
for _, to := range reg.Nodes {
|
|
|
|
pair := nodePair{from.Name, to.Name}
|
|
|
|
addPairMeta(nodePair{from.Name, to.Name})
|
|
|
|
st, ok := state[pair]
|
|
|
|
|
|
|
|
age := now.Sub(st.at).Round(time.Second)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
|
|
|
case !ok:
|
|
|
|
|
|
|
|
o.addBadf("no state for %v", pair)
|
|
|
|
|
|
|
|
case st.err != nil:
|
|
|
|
|
|
|
|
o.addBadf("%v: %v", pair, st.err)
|
|
|
|
|
|
|
|
case age > 90*time.Second:
|
|
|
|
|
|
|
|
o.addBadf("%v: update is %v old", pair, age)
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -117,7 +124,8 @@ func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type nodePair struct {
|
|
|
|
type nodePair struct {
|
|
|
|
from, to string // DERPNode.Name
|
|
|
|
from string // DERPNode.Name, or "UDP" for a STUN query to 'to'
|
|
|
|
|
|
|
|
to string // DERPNode.Name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
|
|
|
|
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
|
|
|
@ -177,6 +185,8 @@ func probe() error {
|
|
|
|
go func() {
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
defer wg.Done()
|
|
|
|
for _, from := range reg.Nodes {
|
|
|
|
for _, from := range reg.Nodes {
|
|
|
|
|
|
|
|
latency, err := probeUDP(ctx, dm, from)
|
|
|
|
|
|
|
|
setState(nodePair{"UDP", from.Name}, latency, err)
|
|
|
|
for _, to := range reg.Nodes {
|
|
|
|
for _, to := range reg.Nodes {
|
|
|
|
latency, err := probeNodePair(ctx, dm, from, to)
|
|
|
|
latency, err := probeNodePair(ctx, dm, from, to)
|
|
|
|
setState(nodePair{from.Name, to.Name}, latency, err)
|
|
|
|
setState(nodePair{from.Name, to.Name}, latency, err)
|
|
|
@ -189,6 +199,65 @@ func probe() error {
|
|
|
|
return ctx.Err()
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func probeUDP(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (latency time.Duration, err error) {
|
|
|
|
|
|
|
|
pc, err := net.ListenPacket("udp", ":0")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
defer pc.Close()
|
|
|
|
|
|
|
|
uc := pc.(*net.UDPConn)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tx := stun.NewTxID()
|
|
|
|
|
|
|
|
req := stun.Request(tx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for _, ipStr := range []string{n.IPv4, n.IPv6} {
|
|
|
|
|
|
|
|
if ipStr == "" {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
port := n.STUNPort
|
|
|
|
|
|
|
|
if port == -1 {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if port == 0 {
|
|
|
|
|
|
|
|
port = 3478
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
|
|
|
|
|
|
_, err := uc.WriteToUDP(req, &net.UDPAddr{IP: ip, Port: port})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
buf := make([]byte, 1500)
|
|
|
|
|
|
|
|
uc.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
|
|
|
|
|
|
t0 := time.Now()
|
|
|
|
|
|
|
|
n, _, err := uc.ReadFromUDP(buf)
|
|
|
|
|
|
|
|
d := time.Since(t0)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
|
|
|
|
return 0, fmt.Errorf("timeout reading from %v: %v", ip)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if d < time.Second {
|
|
|
|
|
|
|
|
return 0, fmt.Errorf("error reading from %v: %v", ip, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
txBack, _, _, err := stun.ParseResponse(buf[:n])
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return 0, fmt.Errorf("parsing STUN response from %v: %v", ip, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if txBack != tx {
|
|
|
|
|
|
|
|
return 0, fmt.Errorf("read wrong tx back from %v", ip)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if latency == 0 || d < latency {
|
|
|
|
|
|
|
|
latency = d
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return latency, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
|
|
|
|
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
|
|
|
|
// The passed in context is a minute for the whole region. The
|
|
|
|
// The passed in context is a minute for the whole region. The
|
|
|
|
// idea is that each node pair in the region will be done
|
|
|
|
// idea is that each node pair in the region will be done
|
|
|
|