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") + } +}