wgengine: add pinger to generate initial spray packets

For 3 seconds after a successful handshake, wgengine will send a
ping packet every 300ms to its peer. This ensures the spray logic
in magicsock has something to spray.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
This commit is contained in:
David Crawshaw 2020-02-25 11:06:29 -05:00 committed by David Crawshaw
parent 3988ddc85d
commit 7a3be96199
2 changed files with 108 additions and 6 deletions

2
go.mod
View File

@ -17,7 +17,7 @@ require (
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/tailscale/hujson v0.0.0-20190930033718-5098e564d9b3
github.com/tailscale/winipcfg-go v0.0.0-20200213045944-185b07f8233f
github.com/tailscale/wireguard-go v0.0.0-20200224122332-ad79bbddc844
github.com/tailscale/wireguard-go v0.0.0-20200225153850-22c50ed0a086
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20200217220822-9197077df867

View File

@ -6,8 +6,10 @@
import (
"bufio"
"context"
"fmt"
"log"
"net"
"strconv"
"strings"
"sync"
@ -38,11 +40,13 @@ type userspaceEngine struct {
wgLock sync.Mutex // serializes all wgdev operations
lastReconfig string
lastCfg wgcfg.Config
lastRoutes string
mu sync.Mutex
peerSequence []wgcfg.Key
endpoints []string
pingers map[wgcfg.Key]context.CancelFunc // mu must be held to call CancelFunc
}
type Loggify struct {
@ -98,6 +102,7 @@ func newUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen R
reqCh: make(chan struct{}, 1),
waitCh: make(chan struct{}),
tundev: tundev,
pingers: make(map[wgcfg.Key]context.CancelFunc),
}
mon, err := monitor.New(logf, func() { e.LinkChange(false) })
@ -141,7 +146,7 @@ func newUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen R
Logger: &logger,
FilterIn: nofilter,
FilterOut: nofilter,
HandshakeDone: func() {
HandshakeDone: func(peerKey wgcfg.Key, allowedIPs []net.IPNet) {
// Send an unsolicited status event every time a
// handshake completes. This makes sure our UI can
// update quickly as soon as it connects to a peer.
@ -151,6 +156,22 @@ func newUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen R
// into it, and wireguard is what called us to get
// here.
go e.RequestStatus()
// All nodes have one primary IP address, and it
// is the first entry on the AllowedIPs list.
//
// This code is written defensively in case we ever
// end up with an empty AllowedIPs list or somehow
// have a subnet as the first entry.
if len(allowedIPs) > 0 {
if ones, bits := allowedIPs[0].Mask.Size(); ones == bits && ones != 0 {
var ip wgcfg.IP
copy(ip.Addr[:], allowedIPs[0].IP.To16())
e.startPinger(peerKey, ip)
return
}
}
logf("ERROR: peer %s has unexpected AllowedIPs: %v", peerKey.ShortString(), allowedIPs)
},
CreateBind: func(uint16) (conn.Bind, uint16, error) {
return e.magicConn, e.magicConn.LocalPort(), nil
@ -205,6 +226,77 @@ func newUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen R
return e, nil
}
// startPinger starts a goroutine that sends ping packets for a few seconds.
//
// These generated packets are used to ensure we trigger the spray logic in
// the magicsock package for NAT traversal.
func (e *userspaceEngine) startPinger(peerKey wgcfg.Key, ip wgcfg.IP) {
e.logf("generating initial ping traffic to %s (%v)", peerKey.ShortString(), ip)
var srcIP packet.IP
e.wgLock.Lock()
if len(e.lastCfg.Addresses) > 0 {
srcIP = packet.NewIP(e.lastCfg.Addresses[0].IP.IP())
}
e.wgLock.Unlock()
if srcIP == 0 {
e.logf("generating initial ping traffic: no source IP")
return
}
e.mu.Lock()
if cancel := e.pingers[peerKey]; cancel != nil {
cancel()
}
ctx, cancel := context.WithCancel(context.Background())
e.pingers[peerKey] = cancel
e.mu.Unlock()
// sendFreq is slightly longer than sprayFreq in magicsock to ensure
// that if these ping packets are the only source of early packets
// sent to the peer, that each one will be sprayed.
const sendFreq = 300 * time.Millisecond
const stopAfter = 3 * time.Second
start := time.Now()
dstIP := packet.NewIP(ip.IP())
payload := []byte("magicsock_spray") // no meaning
go func() {
defer func() {
e.mu.Lock()
defer e.mu.Unlock()
select {
case <-ctx.Done():
return
default:
}
// If the pinger context is not done, then the
// CancelFunc is still in the pingers map.
delete(e.pingers, peerKey)
}()
ipid := uint16(1)
t := time.NewTicker(sendFreq)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
}
if time.Since(start) > stopAfter {
return
}
b := packet.GenICMP(srcIP, dstIP, ipid, packet.EchoRequest, 0, payload)
ipid++
e.wgdev.SendPacket(b)
}
}()
}
// TODO(apenwarr): dnsDomains really ought to be in wgcfg.Config.
// However, we don't actually ever provide it to wireguard and it's not in
// the traditional wireguard config format. On the other hand, wireguard
@ -214,10 +306,12 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, dnsDomains []string) error
e.wgLock.Lock()
defer e.wgLock.Unlock()
e.mu.Lock()
e.peerSequence = make([]wgcfg.Key, len(cfg.Peers))
for i, p := range cfg.Peers {
e.peerSequence[i] = p.PublicKey
}
e.mu.Unlock()
// TODO(apenwarr): get rid of uapi stuff for in-process comms
uapi, err := cfg.ToUAPI()
@ -231,6 +325,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, dnsDomains []string) error
return nil
}
e.lastReconfig = rc
e.lastCfg = cfg.Copy()
if err := e.wgdev.Reconfig(cfg); err != nil {
e.logf("wgdev.Reconfig: %v\n", err)
@ -458,6 +553,13 @@ func (e *userspaceEngine) RequestStatus() {
}
func (e *userspaceEngine) Close() {
e.mu.Lock()
for key, cancel := range e.pingers {
delete(e.pingers, key)
cancel()
}
e.mu.Unlock()
r := bufio.NewReader(strings.NewReader(""))
e.wgdev.IpcSetOperation(r)
e.linkMon.Close()