From 7a3be96199759987773c26859d4478bbbda6eafe Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Tue, 25 Feb 2020 11:06:29 -0500 Subject: [PATCH] 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 --- go.mod | 2 +- wgengine/userspace.go | 112 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 0d74b74b5..7adbf581d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/wgengine/userspace.go b/wgengine/userspace.go index dbb2cc614..c1535a736 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -6,8 +6,10 @@ package wgengine 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 { @@ -94,10 +98,11 @@ func NewUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen R func newUserspaceEngineAdvanced(logf logger.Logf, tundev tun.Device, routerGen RouterGen, listenPort uint16) (_ Engine, reterr error) { e := &userspaceEngine{ - logf: logf, - reqCh: make(chan struct{}, 1), - waitCh: make(chan struct{}), - tundev: tundev, + logf: logf, + 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()