Quellcode durchsuchen

wgengine/magicsock: add new endpoint type used for discovery-supporting peers

This adds a new magicsock endpoint type only used when both sides
support discovery (that is, are advertising a discovery
key). Otherwise the old code is used.

So far the new code only communicates over DERP as proof that the new
code paths are wired up. None of the actually discovery messaging is
implemented yet.

Support for discovery (generating and advertising a key) are still
behind an environment variable for now.

Updates #483
Brad Fitzpatrick vor 5 Jahren
Ursprung
Commit
a975e86bb8

+ 43 - 3
control/controlclient/direct.go

@@ -451,8 +451,6 @@ func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed boo
 	return c.newEndpoints(localPort, endpoints)
 	return c.newEndpoints(localPort, endpoints)
 }
 }
 
 
-var debugNetmap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETMAP"))
-
 func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
 func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
 	c.mu.Lock()
 	c.mu.Lock()
 	persist := c.persist
 	persist := c.persist
@@ -472,7 +470,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
 	c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
 	c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
 
 
 	vlogf := logger.Discard
 	vlogf := logger.Discard
-	if debugNetmap {
+	if Debug.NetMap {
 		vlogf = c.logf
 		vlogf = c.logf
 	}
 	}
 
 
@@ -603,6 +601,18 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
 		if resp.Debug != nil && resp.Debug.LogHeapPprof {
 		if resp.Debug != nil && resp.Debug.LogHeapPprof {
 			go logheap.LogHeap(resp.Debug.LogHeapURL)
 			go logheap.LogHeap(resp.Debug.LogHeapURL)
 		}
 		}
+		// Temporarily (2020-06-29) support removing all but
+		// discovery-supporting nodes during development, for
+		// less noise.
+		if Debug.OnlyDisco {
+			filtered := resp.Peers[:0]
+			for _, p := range resp.Peers {
+				if !p.DiscoKey.IsZero() {
+					filtered = append(filtered, p)
+				}
+			}
+			resp.Peers = filtered
+		}
 
 
 		nm := &NetworkMap{
 		nm := &NetworkMap{
 			NodeKey:      tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
 			NodeKey:      tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
@@ -766,3 +776,33 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
 	}
 	}
 	return key, nil
 	return key, nil
 }
 }
+
+// Debug contains temporary internal-only debug knobs.
+// They're unexported to not draw attention to them.
+var Debug = initDebug()
+
+type debug struct {
+	NetMap    bool
+	OnlyDisco bool
+	Disco     bool
+}
+
+func initDebug() debug {
+	return debug{
+		NetMap:    envBool("TS_DEBUG_NETMAP"),
+		OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
+		Disco:     os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
+	}
+}
+
+func envBool(k string) bool {
+	e := os.Getenv(k)
+	if e == "" {
+		return false
+	}
+	v, err := strconv.ParseBool(os.Getenv("TS_DEBUG_NETMAP"))
+	if err != nil {
+		panic(fmt.Sprintf("invalid non-bool %q for env var %q", e, k))
+	}
+	return v
+}

+ 29 - 18
control/controlclient/netmap.go

@@ -204,6 +204,11 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, uflags int, dnsOverride []wgcfg.IP
 	return wgcfg.FromWgQuick(s, "tailscale")
 	return wgcfg.FromWgQuick(s, "tailscale")
 }
 }
 
 
+// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
+// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
+// This form is then recognize by magicsock's CreateEndpoint.
+const EndpointDiscoSuffix = ".disco.tailscale:12345"
+
 func (nm *NetworkMap) _WireGuardConfig(logf logger.Logf, uflags int, dnsOverride []wgcfg.IP, allEndpoints bool) string {
 func (nm *NetworkMap) _WireGuardConfig(logf logger.Logf, uflags int, dnsOverride []wgcfg.IP, allEndpoints bool) string {
 	buf := new(strings.Builder)
 	buf := new(strings.Builder)
 	fmt.Fprintf(buf, "[Interface]\n")
 	fmt.Fprintf(buf, "[Interface]\n")
@@ -229,6 +234,9 @@ func (nm *NetworkMap) _WireGuardConfig(logf logger.Logf, uflags int, dnsOverride
 	fmt.Fprintf(buf, "\n")
 	fmt.Fprintf(buf, "\n")
 
 
 	for i, peer := range nm.Peers {
 	for i, peer := range nm.Peers {
+		if Debug.OnlyDisco && peer.DiscoKey.IsZero() {
+			continue
+		}
 		if (uflags&UAllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
 		if (uflags&UAllowSingleHosts) == 0 && len(peer.AllowedIPs) < 2 {
 			logf("wgcfg: %v skipping a single-host peer.\n", peer.Key.ShortString())
 			logf("wgcfg: %v skipping a single-host peer.\n", peer.Key.ShortString())
 			continue
 			continue
@@ -239,25 +247,28 @@ func (nm *NetworkMap) _WireGuardConfig(logf logger.Logf, uflags int, dnsOverride
 		fmt.Fprintf(buf, "[Peer]\n")
 		fmt.Fprintf(buf, "[Peer]\n")
 		fmt.Fprintf(buf, "PublicKey = %s\n", base64.StdEncoding.EncodeToString(peer.Key[:]))
 		fmt.Fprintf(buf, "PublicKey = %s\n", base64.StdEncoding.EncodeToString(peer.Key[:]))
 		var endpoints []string
 		var endpoints []string
-		if peer.DERP != "" {
-			endpoints = append(endpoints, peer.DERP)
-		}
-		endpoints = append(endpoints, peer.Endpoints...)
-		if len(endpoints) > 0 {
-			if len(endpoints) == 1 {
-				fmt.Fprintf(buf, "Endpoint = %s", endpoints[0])
-			} else if allEndpoints {
-				// TODO(apenwarr): This mode is incompatible.
-				// Normal wireguard clients don't know how to
-				// parse it (yet?)
-				fmt.Fprintf(buf, "Endpoint = %s",
-					strings.Join(endpoints, ","))
-			} else {
-				fmt.Fprintf(buf, "Endpoint = %s # other endpoints: %s",
-					endpoints[0],
-					strings.Join(endpoints[1:], ", "))
+		if !peer.DiscoKey.IsZero() {
+			fmt.Fprintf(buf, "Endpoint = %x%s\n", peer.DiscoKey[:], EndpointDiscoSuffix)
+		} else {
+			if peer.DERP != "" {
+				endpoints = append(endpoints, peer.DERP)
+			}
+			endpoints = append(endpoints, peer.Endpoints...)
+			if len(endpoints) > 0 {
+				if len(endpoints) == 1 {
+					fmt.Fprintf(buf, "Endpoint = %s", endpoints[0])
+				} else if allEndpoints {
+					// TODO(apenwarr): This mode is incompatible.
+					// Normal wireguard clients don't know how to
+					// parse it (yet?)
+					fmt.Fprintf(buf, "Endpoint = %s", strings.Join(endpoints, ","))
+				} else {
+					fmt.Fprintf(buf, "Endpoint = %s # other endpoints: %s",
+						endpoints[0],
+						strings.Join(endpoints[1:], ", "))
+				}
+				buf.WriteByte('\n')
 			}
 			}
-			buf.WriteByte('\n')
 		}
 		}
 		var aips []string
 		var aips []string
 		for _, allowedIP := range peer.AllowedIPs {
 		for _, allowedIP := range peer.AllowedIPs {

+ 1 - 3
ipn/local.go

@@ -8,8 +8,6 @@ import (
 	"context"
 	"context"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"os"
-	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -362,7 +360,7 @@ func (b *LocalBackend) Start(opts Options) error {
 	b.updateFilter(nil)
 	b.updateFilter(nil)
 
 
 	var discoPublic tailcfg.DiscoKey
 	var discoPublic tailcfg.DiscoKey
-	if useDisco, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_USE_DISCO")); useDisco {
+	if controlclient.Debug.Disco {
 		discoPrivate := key.NewPrivate()
 		discoPrivate := key.NewPrivate()
 		b.e.SetDiscoPrivateKey(discoPrivate)
 		b.e.SetDiscoPrivateKey(discoPrivate)
 		discoPublic = tailcfg.DiscoKey(discoPrivate.Public())
 		discoPublic = tailcfg.DiscoKey(discoPrivate.Public())

+ 207 - 24
wgengine/magicsock/magicsock.go

@@ -28,6 +28,7 @@ import (
 	"github.com/tailscale/wireguard-go/conn"
 	"github.com/tailscale/wireguard-go/conn"
 	"github.com/tailscale/wireguard-go/device"
 	"github.com/tailscale/wireguard-go/device"
 	"github.com/tailscale/wireguard-go/wgcfg"
 	"github.com/tailscale/wireguard-go/wgcfg"
+	"go4.org/mem"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/time/rate"
 	"golang.org/x/time/rate"
 	"inet.af/netaddr"
 	"inet.af/netaddr"
@@ -80,8 +81,8 @@ type Conn struct {
 	// ============================================================
 	// ============================================================
 	mu sync.Mutex // guards all following fields
 	mu sync.Mutex // guards all following fields
 
 
-	started bool
-	closed  bool
+	started bool // Start was called
+	closed  bool // Close was called
 
 
 	endpointsUpdateWaiter *sync.Cond
 	endpointsUpdateWaiter *sync.Cond
 	endpointsUpdateActive bool
 	endpointsUpdateActive bool
@@ -90,9 +91,11 @@ type Conn struct {
 	peerSet               map[key.Public]struct{}
 	peerSet               map[key.Public]struct{}
 
 
 	discoPrivate key.Private
 	discoPrivate key.Private
-	nodeOfDisco  map[tailcfg.DiscoKey]tailcfg.NodeKey
+	nodeOfDisco  map[tailcfg.DiscoKey]*tailcfg.Node
 	discoOfNode  map[tailcfg.NodeKey]tailcfg.DiscoKey
 	discoOfNode  map[tailcfg.NodeKey]tailcfg.DiscoKey
 
 
+	endpointOfDisco map[tailcfg.DiscoKey]*discoEndpoint
+
 	// addrsByUDP is a map of every remote ip:port to a priority
 	// addrsByUDP is a map of every remote ip:port to a priority
 	// list of endpoint addresses for a peer.
 	// list of endpoint addresses for a peer.
 	// The priority list is provided by wgengine configuration.
 	// The priority list is provided by wgengine configuration.
@@ -239,13 +242,14 @@ func (o *Options) endpointsFunc() func([]string) {
 // of NewConn. Mostly for tests.
 // of NewConn. Mostly for tests.
 func newConn() *Conn {
 func newConn() *Conn {
 	c := &Conn{
 	c := &Conn{
-		sendLogLimit: rate.NewLimiter(rate.Every(1*time.Minute), 1),
-		addrsByUDP:   make(map[netaddr.IPPort]*AddrSet),
-		addrsByKey:   make(map[key.Public]*AddrSet),
-		derpRecvCh:   make(chan derpReadResult),
-		udpRecvCh:    make(chan udpReadResult),
-		derpStarted:  make(chan struct{}),
-		peerLastDerp: make(map[key.Public]int),
+		sendLogLimit:    rate.NewLimiter(rate.Every(1*time.Minute), 1),
+		addrsByUDP:      make(map[netaddr.IPPort]*AddrSet),
+		addrsByKey:      make(map[key.Public]*AddrSet),
+		derpRecvCh:      make(chan derpReadResult),
+		udpRecvCh:       make(chan udpReadResult),
+		derpStarted:     make(chan struct{}),
+		peerLastDerp:    make(map[key.Public]int),
+		endpointOfDisco: make(map[tailcfg.DiscoKey]*discoEndpoint),
 	}
 	}
 	c.endpointsUpdateWaiter = sync.NewCond(&c.mu)
 	c.endpointsUpdateWaiter = sync.NewCond(&c.mu)
 	return c
 	return c
@@ -732,6 +736,8 @@ func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
 	switch v := ep.(type) {
 	switch v := ep.(type) {
 	default:
 	default:
 		panic(fmt.Sprintf("[unexpected] Endpoint type %T", v))
 		panic(fmt.Sprintf("[unexpected] Endpoint type %T", v))
+	case *discoEndpoint:
+		return v.send(b)
 	case *singleEndpoint:
 	case *singleEndpoint:
 		addr := (*net.UDPAddr)(v)
 		addr := (*net.UDPAddr)(v)
 		if addr.IP.Equal(derpMagicIP) {
 		if addr.IP.Equal(derpMagicIP) {
@@ -1179,14 +1185,25 @@ func (c *Conn) awaitUDP4(b []byte) {
 		}
 		}
 		return
 		return
 	}
 	}
+}
 
 
+// wgRecvAddr conditionally alters the returned UDPAddr we tell
+// wireguard-go we received a packet from. For peers with discovery
+// keys, we always use the same one, a unique synthetic value created
+// per peer.
+func wgRecvAddr(e conn.Endpoint, addr *net.UDPAddr) *net.UDPAddr {
+	if de, ok := e.(*discoEndpoint); ok {
+		return de.fakeWGAddr
+	}
+	return addr
 }
 }
 
 
 func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr, err error) {
 func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr, err error) {
 	// First, process any buffered packet from earlier.
 	// First, process any buffered packet from earlier.
 	if addr := c.bufferedIPv4From; addr != nil {
 	if addr := c.bufferedIPv4From; addr != nil {
 		c.bufferedIPv4From = nil
 		c.bufferedIPv4From = nil
-		return copy(b, c.bufferedIPv4Packet), c.findEndpoint(addr), addr, nil
+		ep := c.findEndpoint(addr)
+		return copy(b, c.bufferedIPv4Packet), ep, wgRecvAddr(ep, addr), nil
 	}
 	}
 
 
 	go c.awaitUDP4(b)
 	go c.awaitUDP4(b)
@@ -1196,6 +1213,7 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr
 	// completed a successful receive on udpRecvCh.
 	// completed a successful receive on udpRecvCh.
 
 
 	var addrSet *AddrSet
 	var addrSet *AddrSet
+	var discoEp *discoEndpoint
 
 
 	select {
 	select {
 	case dm := <-c.derpRecvCh:
 	case dm := <-c.derpRecvCh:
@@ -1229,10 +1247,15 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr
 		}
 		}
 
 
 		c.mu.Lock()
 		c.mu.Lock()
-		addrSet = c.addrsByKey[dm.src]
+		if dk, ok := c.discoOfNode[tailcfg.NodeKey(dm.src)]; ok {
+			discoEp = c.endpointOfDisco[dk]
+		}
+		if discoEp == nil {
+			addrSet = c.addrsByKey[dm.src]
+		}
 		c.mu.Unlock()
 		c.mu.Unlock()
 
 
-		if addrSet == nil {
+		if addrSet == nil && discoEp == nil {
 			key := wgcfg.Key(dm.src)
 			key := wgcfg.Key(dm.src)
 			c.logf("magicsock: DERP packet from unknown key: %s", key.ShortString())
 			c.logf("magicsock: DERP packet from unknown key: %s", key.ShortString())
 		}
 		}
@@ -1259,10 +1282,12 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr
 
 
 	if addrSet != nil {
 	if addrSet != nil {
 		ep = addrSet
 		ep = addrSet
+	} else if discoEp != nil {
+		ep = discoEp
 	} else {
 	} else {
 		ep = c.findEndpoint(addr)
 		ep = c.findEndpoint(addr)
 	}
 	}
-	return n, ep, addr, nil
+	return n, ep, wgRecvAddr(ep, addr), nil
 }
 }
 
 
 func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) {
 func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) {
@@ -1280,7 +1305,7 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) {
 			continue
 			continue
 		}
 		}
 		ep := c.findEndpoint(addr)
 		ep := c.findEndpoint(addr)
-		return n, ep, addr, nil
+		return n, ep, wgRecvAddr(ep, addr), nil
 	}
 	}
 }
 }
 
 
@@ -1310,7 +1335,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, addr *net.UDPAddr) bool {
 		return false
 		return false
 	}
 	}
 
 
-	senderNodeKey, ok := c.nodeOfDisco[sender]
+	senderNode, ok := c.nodeOfDisco[sender]
 	if !ok {
 	if !ok {
 		// Returning false keeps passing it down, to WireGuard.
 		// Returning false keeps passing it down, to WireGuard.
 		// WireGuard will almost surely reject it, but give it a chance.
 		// WireGuard will almost surely reject it, but give it a chance.
@@ -1325,11 +1350,11 @@ func (c *Conn) handleDiscoMessage(msg []byte, addr *net.UDPAddr) bool {
 	sealedBox := msg[headerLen:]
 	sealedBox := msg[headerLen:]
 	payload, ok := box.Open(nil, sealedBox, &nonce, key.Public(sender).B32(), c.discoPrivate.B32())
 	payload, ok := box.Open(nil, sealedBox, &nonce, key.Public(sender).B32(), c.discoPrivate.B32())
 	if !ok {
 	if !ok {
-		c.logf("magicsock: failed to open disco message box purportedly from %s (disco key %x)", senderNodeKey.ShortString(), sender[:])
+		c.logf("magicsock: failed to open disco message box purportedly from %s (disco key %x)", senderNode.Key.ShortString(), sender[:])
 		return false
 		return false
 	}
 	}
 
 
-	c.logf("magicsock: got disco message from %s: %x (%q)", senderNodeKey.ShortString(), payload, payload)
+	c.logf("magicsock: got disco message from %s: %x (%q)", senderNode.Key.ShortString(), payload, payload)
 	return true
 	return true
 }
 }
 
 
@@ -1427,8 +1452,12 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
 
 
 	numDisco := 0
 	numDisco := 0
 	for _, n := range nm.Peers {
 	for _, n := range nm.Peers {
-		if !n.DiscoKey.IsZero() {
-			numDisco++
+		if n.DiscoKey.IsZero() {
+			continue
+		}
+		numDisco++
+		if ep, ok := c.endpointOfDisco[n.DiscoKey]; ok {
+			ep.updateFromNode(n)
 		}
 		}
 	}
 	}
 
 
@@ -1439,12 +1468,12 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
 	// the set of discokeys changed.
 	// the set of discokeys changed.
 	for pass := 1; pass <= 2; pass++ {
 	for pass := 1; pass <= 2; pass++ {
 		if c.nodeOfDisco == nil || pass == 2 {
 		if c.nodeOfDisco == nil || pass == 2 {
-			c.nodeOfDisco = map[tailcfg.DiscoKey]tailcfg.NodeKey{}
+			c.nodeOfDisco = map[tailcfg.DiscoKey]*tailcfg.Node{}
 			c.discoOfNode = map[tailcfg.NodeKey]tailcfg.DiscoKey{}
 			c.discoOfNode = map[tailcfg.NodeKey]tailcfg.DiscoKey{}
 		}
 		}
 		for _, n := range nm.Peers {
 		for _, n := range nm.Peers {
 			if !n.DiscoKey.IsZero() {
 			if !n.DiscoKey.IsZero() {
-				c.nodeOfDisco[n.DiscoKey] = n.Key
+				c.nodeOfDisco[n.DiscoKey] = n
 				if old, ok := c.discoOfNode[n.Key]; ok && old != n.DiscoKey {
 				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])
 					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.
 					// TODO: reset AddrSet states, reset wireguard session key, etc.
@@ -1457,6 +1486,14 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
 		}
 		}
 	}
 	}
 
 
+	// Clean c.endpointOfDisco for discovery keys that are no longer present.
+	for dk, de := range c.endpointOfDisco {
+		if _, ok := c.nodeOfDisco[dk]; !ok {
+			de.cleanup()
+			delete(c.endpointOfDisco, dk)
+		}
+	}
+
 }
 }
 
 
 func (c *Conn) wantDerpLocked() bool { return c.derpMap != nil }
 func (c *Conn) wantDerpLocked() bool { return c.derpMap != nil }
@@ -1776,9 +1813,15 @@ func (c *Conn) resetAddrSetStates() {
 		as.curAddr = -1
 		as.curAddr = -1
 		as.stopSpray = as.timeNow().Add(sprayPeriod)
 		as.stopSpray = as.timeNow().Add(sprayPeriod)
 	}
 	}
+	for _, de := range c.endpointOfDisco {
+		de.noteConnectivityChange()
+	}
 }
 }
 
 
 // AddrSet is a set of UDP addresses that implements wireguard/conn.Endpoint.
 // AddrSet is a set of UDP addresses that implements wireguard/conn.Endpoint.
+//
+// This is the legacy endpoint for peers that don't support discovery;
+// it predates discoEndpoint.
 type AddrSet struct {
 type AddrSet struct {
 	publicKey key.Public // peer public key used for DERP communication
 	publicKey key.Public // peer public key used for DERP communication
 
 
@@ -2018,11 +2061,39 @@ func (c *Conn) CreateBind(uint16) (conn.Bind, uint16, error) {
 }
 }
 
 
 // CreateEndpoint is called by WireGuard to connect to an endpoint.
 // CreateEndpoint is called by WireGuard to connect to an endpoint.
-// The key is the public key of the peer and addrs is a
-// comma-separated list of UDP ip:ports.
+//
+// The key is the public key of the peer and addrs is either:
+//
+//  1) a comma-separated list of UDP ip:ports (the the peer doesn't have a discovery key)
+//  2) "<hex-discovery-key>.disco.tailscale:12345", a magic value that means the peer
+//     is running code that supports active discovery, so CreateEndpoint returns
+//     a discoEndpoint.
+//
+
 func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, error) {
 func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, error) {
 	pk := key.Public(pubKey)
 	pk := key.Public(pubKey)
 	c.logf("magicsock: CreateEndpoint: key=%s: %s", pk.ShortString(), strings.ReplaceAll(addrs, "127.3.3.40:", "derp-"))
 	c.logf("magicsock: CreateEndpoint: key=%s: %s", pk.ShortString(), strings.ReplaceAll(addrs, "127.3.3.40:", "derp-"))
+
+	if strings.HasSuffix(addrs, controlclient.EndpointDiscoSuffix) {
+		discoHex := strings.TrimSuffix(addrs, controlclient.EndpointDiscoSuffix)
+		discoKey, err := key.NewPublicFromHexMem(mem.S(discoHex))
+		if err != nil {
+			return nil, fmt.Errorf("magicsock: invalid discokey endpoint %q for %v: %w", addrs, pk.ShortString(), err)
+		}
+		c.mu.Lock()
+		defer c.mu.Unlock()
+		de := &discoEndpoint{
+			c:                  c,
+			publicKey:          pk,                         // peer public key (for WireGuard + DERP)
+			discoKey:           tailcfg.DiscoKey(discoKey), // for discovery mesages
+			wgEndpointHostPort: addrs,
+		}
+		de.initFakeUDPAddr()
+		de.updateFromNode(c.nodeOfDisco[de.discoKey])
+		c.endpointOfDisco[de.discoKey] = de
+		return de, nil
+	}
+
 	a := &AddrSet{
 	a := &AddrSet{
 		Logf:      c.logf,
 		Logf:      c.logf,
 		publicKey: pk,
 		publicKey: pk,
@@ -2075,6 +2146,9 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err
 	return a, nil
 	return a, nil
 }
 }
 
 
+// singleEndpoint is a wireguard-go/conn.Endpoint used for "roaming
+// addressed" in releases of Tailscale that predate discovery
+// messages. New peers use discoEndpoint.
 type singleEndpoint net.UDPAddr
 type singleEndpoint net.UDPAddr
 
 
 func (e *singleEndpoint) ClearSrc()           {}
 func (e *singleEndpoint) ClearSrc()           {}
@@ -2250,3 +2324,112 @@ func udpAddrDebugString(ua net.UDPAddr) string {
 	}
 	}
 	return ua.String()
 	return ua.String()
 }
 }
+
+// discoEndpoint is a wireguard/conn.Endpoint for new-style peers that
+// advertise a DiscoKey and participate in active discovery.
+type discoEndpoint struct {
+	c                  *Conn
+	publicKey          key.Public       // peer public key (for WireGuard + DERP)
+	discoKey           tailcfg.DiscoKey // for discovery mesages
+	fakeWGAddr         *net.UDPAddr     // the UDPAddr we tell wireguard-go we're using
+	wgEndpointHostPort string           // string from CreateEndpoint: "<hex-discovery-key>.disco.tailscale:12345"
+
+	mu       sync.Mutex // Lock ordering: Conn.mu, then discoEndpoint.mu
+	derpAddr *net.UDPAddr
+}
+
+// initFakeUDPAddr populates fakeWGAddr with a globally unique fake UDPAddr.
+// The current implementation just uses the pointer value of de jammed into an IPv6
+// address, but it could also be, say, a counter.
+func (de *discoEndpoint) initFakeUDPAddr() {
+	var addr [16]byte
+	addr[0] = 0xfd
+	addr[1] = 0x00
+	binary.BigEndian.PutUint64(addr[2:], uint64(reflect.ValueOf(de).Pointer()))
+	ipp := netaddr.IPPort{
+		IP:   netaddr.IPFrom16(addr),
+		Port: 12345,
+	}
+	de.fakeWGAddr = ipp.UDPAddr()
+}
+
+func (de *discoEndpoint) Addrs() []wgcfg.Endpoint {
+	// This has to be the same string that was passed to
+	// CreateEndpoint, otherwise Reconfig will end up recreating
+	// Endpoints and losing state over time.
+	host, portStr, err := net.SplitHostPort(de.wgEndpointHostPort)
+	if err != nil {
+		panic(err)
+	}
+	port, err := strconv.ParseUint(portStr, 10, 16)
+	if err != nil {
+		panic(err)
+	}
+	return []wgcfg.Endpoint{{host, uint16(port)}}
+}
+
+func (de *discoEndpoint) ClearSrc()           {}
+func (de *discoEndpoint) SrcToString() string { panic("unused") } // unused by wireguard-go
+func (de *discoEndpoint) SrcIP() net.IP       { panic("unused") } // unused by wireguard-go
+func (de *discoEndpoint) DstToString() string { return de.wgEndpointHostPort }
+func (de *discoEndpoint) DstIP() net.IP       { panic("unused") }
+func (de *discoEndpoint) DstToBytes() []byte  { return de.fakeWGAddr.IP[:] }
+func (de *discoEndpoint) UpdateDst(addr *net.UDPAddr) error {
+	// This is called ~per packet (and requiring a mutex acquisition inside wireguard-go).
+	// TODO(bradfitz): make that cheaper and/or remove it. We don't need it.
+	return nil
+}
+
+func (de *discoEndpoint) send(b []byte) error {
+	// TODO: all the disco messaging & state tracking & spraying,
+	// bringing over relevant AddrSet code.  For now, just do DERP
+	// as a crutch while I work on other bits.
+	de.mu.Lock()
+	derpAddr := de.derpAddr
+	de.mu.Unlock()
+
+	if derpAddr == nil {
+		return errors.New("no DERP addr")
+	}
+	return de.c.sendAddr(derpAddr, de.publicKey, b)
+}
+
+func (de *discoEndpoint) updateFromNode(n *tailcfg.Node) {
+	if n == nil {
+		// TODO: log, error, count? if this even happens.
+		return
+	}
+	de.mu.Lock()
+	defer de.mu.Unlock()
+
+	if n.DERP == "" {
+		de.derpAddr = nil
+	} else {
+		// TODO: add ParseIPPort to netaddr package; only safe to use ResolveUDPAddr
+		// here because we know no DNS lookups are involved
+		ua, _ := net.ResolveUDPAddr("udp", n.DERP)
+		de.derpAddr = ua
+	}
+
+	// TODO: parse all the endpoints, not just DERP
+}
+
+// noteConnectivityChange is called when connectivity changes enough
+// that we should question our earlier assumptions about which paths
+// work.
+func (de *discoEndpoint) noteConnectivityChange() {
+	de.mu.Lock()
+	defer de.mu.Unlock()
+
+	// TODO: reset state
+}
+
+// cleanup is called when a discovery endpoint is no longer present in the NetworkMap.
+// This is where we can do cleanup such as closing goroutines or canceling timers.
+func (de *discoEndpoint) cleanup() {
+	de.mu.Lock()
+	defer de.mu.Unlock()
+
+	// TODO: real work later, when there's stuff to do
+	de.c.logf("magicsock: doing cleanup for discovery key %x", de.discoKey[:])
+}

+ 2 - 2
wgengine/magicsock/magicsock_test.go

@@ -844,8 +844,8 @@ func TestDiscoMessage(t *testing.T) {
 	c := &Conn{
 	c := &Conn{
 		logf:         t.Logf,
 		logf:         t.Logf,
 		discoPrivate: key.NewPrivate(),
 		discoPrivate: key.NewPrivate(),
-		nodeOfDisco: map[tailcfg.DiscoKey]tailcfg.NodeKey{
-			tailcfg.DiscoKey(peer1Pub): tailcfg.NodeKey{1: 1},
+		nodeOfDisco: map[tailcfg.DiscoKey]*tailcfg.Node{
+			tailcfg.DiscoKey(peer1Pub): &tailcfg.Node{Key: tailcfg.NodeKey{1: 1}},
 		},
 		},
 	}
 	}