mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
netcheck: add hairpinning detection
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
1abf2da392
commit
bcf3719b9e
@ -25,6 +25,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
|||||||
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
fmt.Printf("\t* UDP: %v\n", report.UDP)
|
||||||
fmt.Printf("\t* IPv6: %v\n", report.IPv6)
|
fmt.Printf("\t* IPv6: %v\n", report.IPv6)
|
||||||
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||||
|
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||||
fmt.Printf("\t* DERP latency:\n")
|
fmt.Printf("\t* DERP latency:\n")
|
||||||
var ss []string
|
var ss []string
|
||||||
for s := range report.DERPLatency {
|
for s := range report.DERPLatency {
|
||||||
|
@ -784,6 +784,7 @@ func (b *LocalBackend) populateNetworkConditions(hi *tailcfg.Hostinfo) {
|
|||||||
ni := &tailcfg.NetInfo{
|
ni := &tailcfg.NetInfo{
|
||||||
DERPLatency: map[string]float64{},
|
DERPLatency: map[string]float64{},
|
||||||
MappingVariesByDestIP: report.MappingVariesByDestIP,
|
MappingVariesByDestIP: report.MappingVariesByDestIP,
|
||||||
|
HairPinning: report.HairPinning,
|
||||||
}
|
}
|
||||||
for server, d := range report.DERPLatency {
|
for server, d := range report.DERPLatency {
|
||||||
ni.DERPLatency[server] = d.Seconds()
|
ni.DERPLatency[server] = d.Seconds()
|
||||||
|
@ -9,12 +9,14 @@
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/interfaces"
|
"tailscale.com/interfaces"
|
||||||
|
"tailscale.com/stun"
|
||||||
"tailscale.com/stunner"
|
"tailscale.com/stunner"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/opt"
|
"tailscale.com/types/opt"
|
||||||
@ -24,10 +26,17 @@ type Report struct {
|
|||||||
UDP bool // UDP works
|
UDP bool // UDP works
|
||||||
IPv6 bool // IPv6 works
|
IPv6 bool // IPv6 works
|
||||||
MappingVariesByDestIP opt.Bool // for IPv4
|
MappingVariesByDestIP opt.Bool // for IPv4
|
||||||
|
HairPinning opt.Bool // for IPv4
|
||||||
DERPLatency map[string]time.Duration // keyed by STUN host:port
|
DERPLatency map[string]time.Duration // keyed by STUN host:port
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
||||||
|
// 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.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
closeOnCtx := func(c io.Closer) {
|
closeOnCtx := func(c io.Closer) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
c.Close()
|
c.Close()
|
||||||
@ -43,6 +52,7 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
DERPLatency: map[string]time.Duration{},
|
DERPLatency: map[string]time.Duration{},
|
||||||
}
|
}
|
||||||
gotIP = map[string]string{} // server -> IP
|
gotIP = map[string]string{} // server -> IP
|
||||||
|
gotIPHair = map[string]string{} // server -> IP for second UDP4 for hairpinning
|
||||||
)
|
)
|
||||||
add := func(server, ip string, d time.Duration) {
|
add := func(server, ip string, d time.Duration) {
|
||||||
logf("%s says we are %s (in %v)", server, ip, d)
|
logf("%s says we are %s (in %v)", server, ip, d)
|
||||||
@ -56,6 +66,11 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
}
|
}
|
||||||
gotIP[server] = ip
|
gotIP[server] = ip
|
||||||
}
|
}
|
||||||
|
addHair := func(server, ip string, d time.Duration) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
gotIPHair[server] = ip
|
||||||
|
}
|
||||||
|
|
||||||
var pc4, pc6 net.PacketConn
|
var pc4, pc6 net.PacketConn
|
||||||
|
|
||||||
@ -65,6 +80,15 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
go closeOnCtx(pc4)
|
go closeOnCtx(pc4)
|
||||||
|
|
||||||
|
// And a second UDP4 socket to check hairpinning.
|
||||||
|
pc4Hair, err := net.ListenPacket("udp4", ":0")
|
||||||
|
if err != nil {
|
||||||
|
logf("udp4: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go closeOnCtx(pc4Hair)
|
||||||
|
|
||||||
if v6 {
|
if v6 {
|
||||||
pc6, err = net.ListenPacket("udp6", ":0")
|
pc6, err = net.ListenPacket("udp6", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,9 +99,9 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := func(s *stunner.Stunner, pc net.PacketConn) {
|
reader := func(s *stunner.Stunner, pc net.PacketConn, maxReads int) {
|
||||||
var buf [64 << 10]byte
|
var buf [64 << 10]byte
|
||||||
for {
|
for i := 0; i < maxReads; i++ {
|
||||||
n, addr, err := pc.ReadFrom(buf[:])
|
n, addr, err := pc.ReadFrom(buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
@ -97,6 +121,8 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var grp errgroup.Group
|
var grp errgroup.Group
|
||||||
|
|
||||||
|
const unlimited = 9999 // effectively, closed on cancel anyway
|
||||||
s4 := &stunner.Stunner{
|
s4 := &stunner.Stunner{
|
||||||
Send: pc4.WriteTo,
|
Send: pc4.WriteTo,
|
||||||
Endpoint: add,
|
Endpoint: add,
|
||||||
@ -104,7 +130,16 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
Logf: logf,
|
Logf: logf,
|
||||||
}
|
}
|
||||||
grp.Go(func() error { return s4.Run(ctx) })
|
grp.Go(func() error { return s4.Run(ctx) })
|
||||||
go reader(s4, pc4)
|
go reader(s4, pc4, unlimited)
|
||||||
|
|
||||||
|
s4Hair := &stunner.Stunner{
|
||||||
|
Send: pc4Hair.WriteTo,
|
||||||
|
Endpoint: addHair,
|
||||||
|
Servers: []string{"derp1.tailscale.com:3478", "derp2.tailscale.com:3478"},
|
||||||
|
Logf: logf,
|
||||||
|
}
|
||||||
|
grp.Go(func() error { return s4Hair.Run(ctx) })
|
||||||
|
go reader(s4Hair, pc4Hair, 2)
|
||||||
|
|
||||||
if v6 {
|
if v6 {
|
||||||
s6 := &stunner.Stunner{
|
s6 := &stunner.Stunner{
|
||||||
@ -115,7 +150,7 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
OnlyIPv6: true,
|
OnlyIPv6: true,
|
||||||
}
|
}
|
||||||
grp.Go(func() error { return s6.Run(ctx) })
|
grp.Go(func() error { return s6.Run(ctx) })
|
||||||
go reader(s6, pc6)
|
go reader(s6, pc6, unlimited)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = grp.Wait()
|
err = grp.Wait()
|
||||||
@ -126,12 +161,29 @@ func GetReport(ctx context.Context, logf logger.Logf) (*Report, error) {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock() // unnecessary, but feels weird without
|
defer mu.Unlock() // unnecessary, but feels weird without
|
||||||
|
|
||||||
|
var checkHairpinning bool
|
||||||
|
|
||||||
// TODO: generalize this to find at least two out of N DERP
|
// TODO: generalize this to find at least two out of N DERP
|
||||||
// servers (where N will be 5+).
|
// servers (where N will be 5+).
|
||||||
ip1 := gotIP["derp1.tailscale.com:3478"]
|
ip1 := gotIP["derp1.tailscale.com:3478"]
|
||||||
ip2 := gotIP["derp2.tailscale.com:3478"]
|
ip2 := gotIP["derp2.tailscale.com:3478"]
|
||||||
if ip1 != "" && ip2 != "" {
|
if ip1 != "" && ip2 != "" {
|
||||||
ret.MappingVariesByDestIP.Set(ip1 != ip2)
|
ret.MappingVariesByDestIP.Set(ip1 != ip2)
|
||||||
|
checkHairpinning = ip1 == ip2 && gotIPHair["derp1.tailscale.com:3478"] != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkHairpinning {
|
||||||
|
hairIPStr, hairPortStr, _ := net.SplitHostPort(gotIPHair["derp1.tailscale.com:3478"])
|
||||||
|
hairIP := net.ParseIP(hairIPStr)
|
||||||
|
hairPort, _ := strconv.Atoi(hairPortStr)
|
||||||
|
if hairIP != nil && hairPort != 0 {
|
||||||
|
tx := stun.NewTxID() // random payload
|
||||||
|
pc4.WriteTo(tx[:], &net.UDPAddr{IP: hairIP, Port: hairPort})
|
||||||
|
var got stun.TxID
|
||||||
|
pc4Hair.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
_, _, err := pc4Hair.ReadFrom(got[:])
|
||||||
|
ret.HairPinning.Set(err == nil && got == tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
|
@ -249,6 +249,10 @@ type NetInfo struct {
|
|||||||
// vary based on the destination IP.
|
// vary based on the destination IP.
|
||||||
MappingVariesByDestIP opt.Bool
|
MappingVariesByDestIP opt.Bool
|
||||||
|
|
||||||
|
// HairPinning is their router does hairpinning.
|
||||||
|
// It reports true even if there's no NAT involved.
|
||||||
|
HairPinning opt.Bool
|
||||||
|
|
||||||
// WorkingIPv6 is whether IPv6 works.
|
// WorkingIPv6 is whether IPv6 works.
|
||||||
WorkingIPv6 opt.Bool
|
WorkingIPv6 opt.Bool
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user