From 103c06cc68161a1ac55cb158ced1e35798d0dec8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 26 Jun 2020 14:38:53 -0700 Subject: [PATCH] wgengine/magicsock: open discovery naclbox messages from known peers And track known peers. Doesn't yet do anything with the messages. (nor does it send any yet) Start of docs on the message format. More will come in subsequent changes. Updates #483 --- wgengine/magicsock/magicsock.go | 90 +++++++++++++++++++++++++++- wgengine/magicsock/magicsock_test.go | 28 +++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 96701146e..a567c463c 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -28,6 +28,7 @@ import ( "github.com/tailscale/wireguard-go/conn" "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/wgcfg" + "golang.org/x/crypto/nacl/box" "golang.org/x/time/rate" "inet.af/netaddr" "tailscale.com/control/controlclient" @@ -76,6 +77,7 @@ type Conn struct { udpRecvCh chan udpReadResult derpRecvCh chan derpReadResult + // ============================================================ mu sync.Mutex // guards all following fields started bool @@ -88,6 +90,8 @@ type Conn struct { peerSet map[key.Public]struct{} discoPrivate key.Private + nodeOfDisco map[tailcfg.DiscoKey]tailcfg.NodeKey + discoOfNode map[tailcfg.NodeKey]tailcfg.DiscoKey // addrsByUDP is a map of every remote ip:port to a priority // list of endpoint addresses for a peer. @@ -1164,6 +1168,9 @@ func (c *Conn) awaitUDP4(b []byte) { c.stunReceiveFunc.Load().(func([]byte, *net.UDPAddr))(b[:n], addr) continue } + if c.handleDiscoMessage(b[:n], addr) { + continue + } addr.IP = addr.IP.To4() select { @@ -1277,6 +1284,55 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) { } } +// handleDiscoMessage reports whether msg was a Tailscale inter-node discovery message +// that was handled. +// +// A discovery message has the form: +// +// * magic [6]byte +// * senderDiscoPubKey [32]byte +// * nonce [24]byte +// * naclbox of payload +func (c *Conn) handleDiscoMessage(msg []byte, addr *net.UDPAddr) bool { + const magic = "TS💬" + const nonceLen = 24 + const headerLen = len(magic) + len(tailcfg.DiscoKey{}) + nonceLen + if len(msg) < headerLen || string(msg[:len(magic)]) != magic { + return false + } + var sender tailcfg.DiscoKey + copy(sender[:], msg[len(magic):]) + + c.mu.Lock() + defer c.mu.Unlock() + + if c.discoPrivate.IsZero() { + return false + } + + senderNodeKey, ok := c.nodeOfDisco[sender] + if !ok { + // Returning false keeps passing it down, to WireGuard. + // WireGuard will almost surely reject it, but give it a chance. + return false + } + + // First, do we even know (and thus care) about this sender? If not, + // don't bother decrypting it. + + var nonce [nonceLen]byte + copy(nonce[:], msg[len(magic)+len(key.Public{}):]) + sealedBox := msg[headerLen:] + payload, ok := box.Open(nil, sealedBox, &nonce, key.Public(sender).B32(), c.discoPrivate.B32()) + if !ok { + c.logf("magicsock: failed to open disco message box purportedly from %s (disco key %x)", senderNodeKey.ShortString(), sender[:]) + return false + } + + c.logf("magicsock: got disco message from %s: %x (%q)", senderNodeKey.ShortString(), payload, payload) + return true +} + // SetPrivateKey sets the connection's private key. // // This is only used to be able prove our identity when connecting to @@ -1368,11 +1424,39 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) { if reflect.DeepEqual(nm, c.netMap) { return } - c.logf("magicsock: got updated network map") + numDisco := 0 + for _, n := range nm.Peers { + if !n.DiscoKey.IsZero() { + numDisco++ + } + } + + c.logf("magicsock: got updated network map; %d peers (%d with discokey)", len(nm.Peers), numDisco) c.netMap = nm - // TODO: look at Debug fields - // TODO: look at DiscoKey fields to reset AddrSet states when node restarts + + // Build and/or update node<->disco maps, only reallocating if + // the set of discokeys changed. + for pass := 1; pass <= 2; pass++ { + if c.nodeOfDisco == nil || pass == 2 { + c.nodeOfDisco = map[tailcfg.DiscoKey]tailcfg.NodeKey{} + c.discoOfNode = map[tailcfg.NodeKey]tailcfg.DiscoKey{} + } + for _, n := range nm.Peers { + if !n.DiscoKey.IsZero() { + c.nodeOfDisco[n.DiscoKey] = n.Key + if old, ok := c.discoOfNode[n.Key]; ok && old != n.DiscoKey { + c.logf("magicsock: node %s changed discovery key from %x to %x", n.Key.ShortString(), old[:8], n.DiscoKey[:8]) + // TODO: reset AddrSet states, reset wireguard session key, etc. + } + c.discoOfNode[n.Key] = n.DiscoKey + } + } + if len(c.nodeOfDisco) == numDisco && len(c.discoOfNode) == numDisco { + break + } + } + } func (c *Conn) wantDerpLocked() bool { return c.derpMap != nil } diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index a2c515a81..6f83af9a4 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -23,6 +23,7 @@ import ( "github.com/tailscale/wireguard-go/device" "github.com/tailscale/wireguard-go/tun/tuntest" "github.com/tailscale/wireguard-go/wgcfg" + "golang.org/x/crypto/nacl/box" "tailscale.com/derp" "tailscale.com/derp/derphttp" "tailscale.com/derp/derpmap" @@ -835,3 +836,30 @@ func TestAddrSet(t *testing.T) { }) } } + +func TestDiscoMessage(t *testing.T) { + peer1Priv := key.NewPrivate() + peer1Pub := peer1Priv.Public() + + c := &Conn{ + logf: t.Logf, + discoPrivate: key.NewPrivate(), + nodeOfDisco: map[tailcfg.DiscoKey]tailcfg.NodeKey{ + tailcfg.DiscoKey(peer1Pub): tailcfg.NodeKey{1: 1}, + }, + } + + const payload = "why hello" + + var nonce [24]byte + crand.Read(nonce[:]) + + pkt := append([]byte("TS💬"), peer1Pub[:]...) + pkt = append(pkt, nonce[:]...) + + pkt = box.Seal(pkt, []byte(payload), &nonce, c.discoPrivate.Public().B32(), peer1Priv.B32()) + got := c.handleDiscoMessage(pkt, &net.UDPAddr{}) + if !got { + t.Error("failed to open it") + } +}