|
|
|
@@ -28,6 +28,7 @@ import (
|
|
|
|
|
"github.com/tailscale/wireguard-go/conn"
|
|
|
|
|
"github.com/tailscale/wireguard-go/device"
|
|
|
|
|
"github.com/tailscale/wireguard-go/wgcfg"
|
|
|
|
|
"go4.org/mem"
|
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
|
|
|
"golang.org/x/time/rate"
|
|
|
|
|
"inet.af/netaddr"
|
|
|
|
@@ -80,8 +81,8 @@ type Conn struct {
|
|
|
|
|
// ============================================================
|
|
|
|
|
mu sync.Mutex // guards all following fields
|
|
|
|
|
|
|
|
|
|
started bool
|
|
|
|
|
closed bool
|
|
|
|
|
started bool // Start was called
|
|
|
|
|
closed bool // Close was called
|
|
|
|
|
|
|
|
|
|
endpointsUpdateWaiter *sync.Cond
|
|
|
|
|
endpointsUpdateActive bool
|
|
|
|
@@ -90,9 +91,11 @@ type Conn struct {
|
|
|
|
|
peerSet map[key.Public]struct{}
|
|
|
|
|
|
|
|
|
|
discoPrivate key.Private
|
|
|
|
|
nodeOfDisco map[tailcfg.DiscoKey]tailcfg.NodeKey
|
|
|
|
|
nodeOfDisco map[tailcfg.DiscoKey]*tailcfg.Node
|
|
|
|
|
discoOfNode map[tailcfg.NodeKey]tailcfg.DiscoKey
|
|
|
|
|
|
|
|
|
|
endpointOfDisco map[tailcfg.DiscoKey]*discoEndpoint
|
|
|
|
|
|
|
|
|
|
// addrsByUDP is a map of every remote ip:port to a priority
|
|
|
|
|
// list of endpoint addresses for a peer.
|
|
|
|
|
// The priority list is provided by wgengine configuration.
|
|
|
|
@@ -239,13 +242,14 @@ func (o *Options) endpointsFunc() func([]string) {
|
|
|
|
|
// of NewConn. Mostly for tests.
|
|
|
|
|
func newConn() *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)
|
|
|
|
|
return c
|
|
|
|
@@ -732,6 +736,8 @@ func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
|
|
|
|
|
switch v := ep.(type) {
|
|
|
|
|
default:
|
|
|
|
|
panic(fmt.Sprintf("[unexpected] Endpoint type %T", v))
|
|
|
|
|
case *discoEndpoint:
|
|
|
|
|
return v.send(b)
|
|
|
|
|
case *singleEndpoint:
|
|
|
|
|
addr := (*net.UDPAddr)(v)
|
|
|
|
|
if addr.IP.Equal(derpMagicIP) {
|
|
|
|
@@ -1179,14 +1185,25 @@ func (c *Conn) awaitUDP4(b []byte) {
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
// First, process any buffered packet from earlier.
|
|
|
|
|
if addr := c.bufferedIPv4From; addr != 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)
|
|
|
|
@@ -1196,6 +1213,7 @@ func (c *Conn) ReceiveIPv4(b []byte) (n int, ep conn.Endpoint, addr *net.UDPAddr
|
|
|
|
|
// completed a successful receive on udpRecvCh.
|
|
|
|
|
|
|
|
|
|
var addrSet *AddrSet
|
|
|
|
|
var discoEp *discoEndpoint
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
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()
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
if addrSet == nil {
|
|
|
|
|
if addrSet == nil && discoEp == nil {
|
|
|
|
|
key := wgcfg.Key(dm.src)
|
|
|
|
|
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 {
|
|
|
|
|
ep = addrSet
|
|
|
|
|
} else if discoEp != nil {
|
|
|
|
|
ep = discoEp
|
|
|
|
|
} else {
|
|
|
|
|
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) {
|
|
|
|
@@ -1280,7 +1305,7 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
senderNodeKey, ok := c.nodeOfDisco[sender]
|
|
|
|
|
senderNode, 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.
|
|
|
|
@@ -1325,11 +1350,11 @@ func (c *Conn) handleDiscoMessage(msg []byte, addr *net.UDPAddr) bool {
|
|
|
|
|
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[:])
|
|
|
|
|
c.logf("magicsock: failed to open disco message box purportedly from %s (disco key %x)", senderNode.Key.ShortString(), sender[:])
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1427,8 +1452,12 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
|
|
|
|
|
|
|
|
|
|
numDisco := 0
|
|
|
|
|
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.
|
|
|
|
|
for pass := 1; pass <= 2; pass++ {
|
|
|
|
|
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{}
|
|
|
|
|
}
|
|
|
|
|
for _, n := range nm.Peers {
|
|
|
|
|
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 {
|
|
|
|
|
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.
|
|
|
|
@@ -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 }
|
|
|
|
@@ -1776,9 +1813,15 @@ func (c *Conn) resetAddrSetStates() {
|
|
|
|
|
as.curAddr = -1
|
|
|
|
|
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.
|
|
|
|
|
//
|
|
|
|
|
// This is the legacy endpoint for peers that don't support discovery;
|
|
|
|
|
// it predates discoEndpoint.
|
|
|
|
|
type AddrSet struct {
|
|
|
|
|
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.
|
|
|
|
|
// 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) {
|
|
|
|
|
pk := key.Public(pubKey)
|
|
|
|
|
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{
|
|
|
|
|
Logf: c.logf,
|
|
|
|
|
publicKey: pk,
|
|
|
|
@@ -2075,6 +2146,9 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
func (e *singleEndpoint) ClearSrc() {}
|
|
|
|
@@ -2250,3 +2324,112 @@ func udpAddrDebugString(ua net.UDPAddr) 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[:])
|
|
|
|
|
}
|
|
|
|
|