We can't do Noise-over-HTTP in Wasm/JS (because we don't have bidirectional
communication), but we should be able to do it over WebSockets. Reuses
derp WebSocket support that allows us to turn a WebSocket connection
into a net.Conn.
Updates #3157
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
Currently, when SetNetInfo is called it sets the value on
hostinfo.NetInfo. However, when SetHostInfo is called it overwrites the
hostinfo field which may mean it also clears out the NetInfo it had just
received.
This commit stores NetInfo separately and combines it into Hostinfo as
needed so that control is always notified of the latest values.
Also, remove unused copies of Hostinfo from ipn.Status and
controlclient.Auto.
Updates #tailscale/corp#4824 (maybe fixes)
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Just because we get an HTTP upgrade response over port 80, don't
assume we'll be able to do bi-di Noise over it. There might be a MITM
corp proxy or anti-virus/firewall interfering. Do a bit more work to
validate the connection before proceeding to give up on the TLS port
443 dial.
Updates #4557 (probably fixes)
Change-Id: I0e1bcc195af21ad3d360ffe79daead730dfd86f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
The connections returned from SystemDial are automatically closed when
there is a major link change.
Also plumb through the dialer to the noise client so that connections
are auto-reset when moving from cellular to WiFi etc.
Updates #3363
Signed-off-by: Maisem Ali <maisem@tailscale.com>
In debugging #4541, I noticed this log print was always empty.
The value printed was always zero at this point.
Updates #4541
Change-Id: I0eef60c32717c293c1c853879446be65d9b2cef6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
For people running self-hosted control planes who want a global
opt-out knob instead of running their own logcatcher.
Change-Id: I7f996c09f45850ff77b58bfd5a535e197971725a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
tailcfg.PingResponse formalizes the TSMP & disco response message, and
controlclient is wired to send POST responses containing
tailcfg.PingResponse for TSMP and disco PingRequests.
Updates tailscale/corp#754
Signed-off-by: James Tucker <james@tailscale.com>
Doing so makes development unpleasant, because we have to first break the
client by bumping to a version the control server rejects, then upgrade
the control server to make it accept the new version.
This strict rejection at handshake time is only necessary if we want to
blocklist some vulnerable protocol versions in the future. So, switch
to a default-permissive stance: until we have such a version that we
have to eagerly block early, we'll accept whatever version the client
presents, and leave it to the user of controlbase.Conn to make decisions
based on that version.
Noise still enforces that the client and server *agree* on what protocol
version is being used, and the control server still has the option to
finish the handshake and then hang up with an in-noise error, rather
than abort at the handshake level.
Updates #3488
Signed-off-by: David Anderson <danderson@tailscale.com>
This is so that we can plumb our client capability version through
the protocol as the Noise version. The capability version increments
more frequently than strictly required (the Noise version only needs
to change when cryptographically-significant changes are made to
the protocol, whereas the capability version also indicates changes
in non-cryptographically-significant parts of the protocol), but this
gives us a safe pre-auth way to determine if the client supports
future protocol features, while still relying on Noise's strong
assurance that the client and server have agreed on the same version.
Currently, the server executes the same protocol regardless of the
version number, and just presents the version to the caller so they
can do capability-based things in the upper RPC protocol. In future,
we may add a ratchet to disallow obsolete protocols, or vary the
Noise handshake behavior based on requested version.
Updates #3488
Signed-off-by: David Anderson <danderson@tailscale.com>
Combine the code between `LocalBackend.CheckIPForwarding` and
`controlclient.ipForwardingBroken`.
Fixes#4300
Signed-off-by: Maisem Ali <maisem@tailscale.com>
It includes a fix to allow us to use Go 1.18.
We can now remove our Tailscale-only build tags.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
The certstore code is impacted by golang/go#51726.
The Tailscale Go toolchain fork contains a temporary workaround,
so it can compile it. Once the upstream toolchain can compile certstore,
presumably in Go 1.18.1, we can revert this change.
Note that depaware runs with the upstream toolchain.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
Currently `Write` returns the number of ciphertext bytes written.
According to the docs for io.Writer, Write should return the amount
of bytes consumed from the input.
```
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
Write(p []byte) (n int, err error)
```
Fixes#4126
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Fix regression from 21069124db caught by tests in another repo.
The HTTP/2 Transport that was being returned had a ConnPool that never
dialed.
Updates #3488
Change-Id: I3184d6393813448ae143d37ece14eb732334c05f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
We want to close the connection after a minute of inactivity,
not heartbeat once a minute to keep it alive forever.
Updates #3488
Change-Id: I4b5275e8d1f2528e13de2d54808773c70537db91
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
And add a CapabilityVersion type, primarily for documentation.
This makes MapRequest.Version, RegisterRequest.Version, and
SetDNSRequest.Version all use the same version, which will avoid
confusing in the future if Register or SetDNS ever changed their
semantics on Version change. (Currently they're both always 1)
This will requre a control server change to allow a
SetDNSRequest.Version value other than 1 to be deployed first.
Change-Id: I073042a216e0d745f52ee2dbc45cf336b9f84b7c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
When I deployed server-side changes, I put the upgrade handler at /ts2021
instead of /switch. We could move the server to /switch, but ts2021 seems
more specific and better, but I don't feel strongly.
Updates #3488
Change-Id: Ifbf8ea60a815fd2fa1bfbe1b7af1ac2a27218354
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Otherwise omitempty doesn't work.
This is wire-compatible with a non-pointer type, so switching
is safe, now and in the future.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
And log it when provided in map responses.
The test uses the date on which I joined Tailscale. :)
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
The TODO is done. Magicsock doesn't require any endpoints to create an
*endpoint now. Verified both in code and empirically: I can use the
env knob and access everything.
Change-Id: I4fe7ed5b11c5c5e94b21ef3d77be149daeab998a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
If multiple certificates match when selecting a certificate, use the one
issued the most recently (as determined by the NotBefore timestamp).
This also adds some tests for the function that performs that
comparison.
Updates tailscale/coral#6
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
We don't use it anyway, so be explicit that we're not using it.
Change-Id: Iec953271ef0169a2e227811932f5b65b479624af
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
A new package can also later record/report which knobs are checked and
set. It also makes the code cleaner & easier to grep for env knobs.
Change-Id: Id8a123ab7539f1fadbd27e0cbeac79c2e4f09751
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
With this change, the client can obtain the initial handshake message
separately from the rest of the handshake, for embedding into another
protocol. This enables things like RTT reduction by stuffing the
handshake initiation message into an HTTP header.
Similarly, the server API optionally accepts a pre-read Noise initiation
message, in addition to reading the message directly off a net.Conn.
Updates #3488
Signed-off-by: David Anderson <danderson@tailscale.com>
Fixes regression from 81cabf48ec which made
all map errors be sent to the frontend UI.
Fixes#3230
Change-Id: I7f142c801c7d15e268a24ddf901c3e6348b6729c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
So if the control plane knows that something's broken about the node, it can
include problem(s) in MapResponse and "tailscale status" will show it.
(and GUIs in the future, as it's in ipnstate.Status/JSON)
This also bumps the MapRequest.Version, though it's not strictly
required. Doesn't hurt.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
* Revert "Revert "types/key: add MachinePrivate and MachinePublic.""
This reverts commit 61c3b98a24.
Signed-off-by: David Anderson <danderson@tailscale.com>
* types/key: add ControlPrivate, with custom serialization.
ControlPrivate is just a MachinePrivate that serializes differently
in JSON, to be compatible with how the Tailscale control plane
historically serialized its private key.
Signed-off-by: David Anderson <danderson@tailscale.com>
Plumb throughout the codebase as a replacement for the mixed use of
tailcfg.MachineKey and wgkey.Private/Public.
Signed-off-by: David Anderson <danderson@tailscale.com>
It was useful early in development when disco clients were the
exception and tailscale logs were noisier than today, but now
non-disco is the exception.
Updates #2752
Signed-off-by: David Anderson <danderson@tailscale.com>
The netmaps can get really large.
Printing, processing, and uploading them is expensive.
Only print the header on an ongoing basis.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
Add in UPnP portmapping, using goupnp library in order to get the UPnP client and run the
portmapping functions. This rips out anywhere where UPnP used to be in portmapping, and has a
flow separate from PMP and PCP.
RELNOTE=portmapper now supports UPnP mappings
Fixes#682
Updates #2109
Signed-off-by: julianknodt <julianknodt@gmail.com>
Turns out we never reliably log the control plane URL a client connects
to. Do it here, and include the server public key, which might
inadvertently tell us something interesting some day.
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
AWS Lambda uses Docker containers but does not
have the string "docker" in its /proc/1/cgroup.
Infer AWS Lambda via the environment variables
it sets.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
Previously, there was no server round trip required to log out, so when
you asked ipnlocal to Logout(), it could clear the netmap immediately
and switch to NeedsLogin state.
In v1.8, we added a true Logout operation. ipn.Logout() would trigger
an async cc.StartLogout() and *also* immediately switch to NeedsLogin.
Unfortunately, some frontends would see NeedsLogin and immediately
trigger a new StartInteractiveLogin() operation, before the
controlclient auth state machine actually acted on the Logout command,
thus accidentally invalidating the entire logout operation, retaining
the netmap, and violating the user's expectations.
Instead, add a new LogoutFinished signal from controlclient
(paralleling LoginFinished) and, upon starting a logout, don't update
the ipn state machine until it's received.
Updates: #1918 (BUG-2)
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
The cyolosecurity fork of certstore did not update its module name and
thus can only be used with a replace directive. This interferes with
installing using `go install` so I created a tailscale fork with an
updated module name.
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Without this, macOS would fail to display its menu state correctly if you
started it while !WantRunning. It relies on the netmap in order to show
the logged-in username.
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
So the NetworkMap-from-incremental-MapResponses can be tested easily.
And because direct.go was getting too big.
No change in behavior at this point. Just movement.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
And fix PeerSeenChange bug where it was ignored unless there were
other peer changes.
Updates tailscale/corp#1574
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Track endpoints internally with a new tailcfg.Endpoint type that
includes a typed netaddr.IPPort (instead of just a string) and
includes a type for how that endpoint was discovered (STUN, local,
etc).
Use []tailcfg.Endpoint instead of []string internally.
At the last second, send it to the control server as the existing
[]string for endpoints, but also include a new parallel
MapRequest.EndpointType []tailcfg.EndpointType, so the control server
can start filtering out less-important endpoint changes from
new-enough clients. Notably, STUN-discovered endpoints can be filtered
out from 1.6+ clients, as they can discover them amongst each other
via CallMeMaybe disco exchanges started over DERP. And STUN endpoints
change a lot, causing a lot of MapResposne updates. But portmapped
endpoints are worth keeping for now, as they they work right away
without requiring the firewall traversal extra RTT dance.
End result will be less control->client bandwidth. (despite negligible
increase in client->control bandwidth)
Updates tailscale/corp#1543
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
When searching for the matching client identity, the returned
certificate chain was accidentally set to that of the last identity
returned by the certificate store instead of the one corresponding to
the selected identity.
Also, add some extra error checking for invalid certificate chains, just
in case.
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Instead of having the CLI check whether IP forwarding is enabled, ask
tailscaled. It has a better idea. If it's netstack, for instance, the
sysctl values don't matter. And it's possible that only the daemon has
permission to know.
Fixes#1626
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
The concrete type being encoded changed from a value to pointer
earlier and this was never adjusted.
(People don't frequently use TS_DEBUG_MAP to see requests, so it went
unnoticed until now.)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
control/controlclient: sign RegisterRequest
Some customers wish to verify eligibility for devices to join their
tailnets using machine identity certificates. TLS client certs could
potentially fulfill this role but the initial customer for this feature
has technical requirements that prevent their use. Instead, the
certificate is loaded from the Windows local machine certificate store
and uses its RSA public key to sign the RegisterRequest message.
There is room to improve the flexibility of this feature in future and
it is currently only tested on Windows (although Darwin theoretically
works too), but this offers a reasonable starting place for now.
Updates tailscale/coral#6
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
The direct client already logs it in JSON form. Then it's immediately
logged again in an unformatted dump, so this removes that unformatted
one.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
To atone for 1d7f9d5b4a, the revert of 4224b3f731.
At least it's fast again, even if it's shelling out to cmd.exe (once now).
Updates #1478
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
IP forwarding is not required when advertising a machine's local IPs
over Tailscale.
Fixes#1435.
Signed-off-by: David Anderson <danderson@tailscale.com>
So the control server can test whether a client's actually present.
Most clients are over HTTP/2, so these pings (to the same host) are
super cheap.
This mimics the earlier goroutine dump mechanism.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This one alone doesn't modify the global dependency map much
(depaware.txt if anything looks slightly worse), but it leave
controlclient as only containing NetworkMap:
bradfitz@tsdev:~/src/tailscale.com/ipn$ grep -F "controlclient." *.go
backend.go: NetMap *controlclient.NetworkMap // new netmap received
fake_test.go: b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
fake_test.go: b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
handle.go: netmapCache *controlclient.NetworkMap
handle.go:func (h *Handle) NetMap() *controlclient.NetworkMap {
Once that goes into a leaf package, then ipn doesn't depend on
controlclient at all, and then the client gets smaller.
Updates #1278
This is mostly code movement from the wireguard-go repo.
Most of the new wgcfg package corresponds to the wireguard-go wgcfg package.
wgengine/wgcfg/device{_test}.go was device/config{_test}.go.
There were substantive but simple changes to device_test.go to remove
internal package device references.
The API of device.Config (now wgcfg.DeviceConfig) grew an error return;
we previously logged the error and threw it away.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Consolidates the node display name logic from each of the clients into
tailcfg.Node. UI clients can use these names directly, rather than computing
them independently.
This eliminates a dependency on wgcfg.Endpoint,
as part of the effort to eliminate our wireguard-go fork.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
* show DNS name over hostname, removing domain's common MagicDNS suffix.
only show hostname if there's no DNS name.
but still show shared devices' MagicDNS FQDN.
* remove nerdy low-level details by default: endpoints, DERP relay,
public key. They're available in JSON mode still for those who need
them.
* only show endpoint or DERP relay when it's active with the goal of
making debugging easier. (so it's easier for users to understand
what's happening) The asterisks are gone.
* remove Tx/Rx numbers by default for idle peers; only show them when
there's traffic.
* include peers' owner login names
* add CLI option to not show peers (matching --self=true, --peers= also
defaults to true)
* sort by DNS/host name, not public key
* reorder columns
Previously, any change to endpoints or hostinfo (or hostinfo's
netinfo) would result in the long-running map request HTTP stream
being torn down and restarted, losing all compression context along
with it.
This change makes us instead send a lite map request (OmitPeers: true,
Stream: false) that doesn't subscribe to anything, and then the
coordination server knows to not close other streams for that node
when it recives a lite request.
Fixestailscale/corp#797
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Previously the client had heuristics to calculate which DNS search domains
to set, based on the peers' names. Unfortunately that prevented us from
doing some things we wanted to do server-side related to node sharing.
So, bump MapRequest.Version to 9 to signal that the client only uses the
explicitly configured DNS search domains and doesn't augment it with its own
list.
Updates tailscale/corp#1026
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This is a replacement for the key-related parts
of the wireguard-go wgcfg package.
This is almost a straight copy/paste from the wgcfg package.
I have slightly changed some of the exported functions and types
to avoid stutter, added and tweaked some comments,
and removed some now-unused code.
To avoid having wireguard-go depend on this new package,
wgcfg will keep its key types.
We translate into and out of those types at the last minute.
These few remaining uses will be eliminated alongside
the rest of the wgcfg package.
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
The fallthrough happened to work in controlclient already due to the
/etc/os-release PRETTY_NAME default, but make it explicit so it
doesn't look like an accident.
Also add it to version/distro, even though nothing needs it yet.
For now, the server will only send v6 configuration to mapversion 8 clients
as part of an early-adopter program, while we verify that the functionality
is robust.
Signed-off-by: David Anderson <danderson@tailscale.com>
Addresses #964
Still to be done:
- Figure out the correct logging lines in util/systemd
- Figure out if we need to slip the systemd.Status function anywhere
else
- Log util/systemd errors? (most of the errors are of the "you cannot do
anything about this, but it might be a bad idea to crash the program if
it errors" kind)
Assistance in getting this over the finish line would help a lot.
Signed-off-by: Christine Dodrill <me@christine.website>
util/systemd: rename the nonlinux file to appease the magic
Signed-off-by: Christine Dodrill <me@christine.website>
util/systemd: fix package name
Signed-off-by: Christine Dodrill <me@christine.website>
util/systemd: fix review feedback from @mdlayher
Signed-off-by: Christine Dodrill <me@christine.website>
cmd/tailscale{,d}: update depaware manifests
Signed-off-by: Christine Dodrill <me@christine.website>
util/systemd: use sync.Once instead of func init
Signed-off-by: Christine Dodrill <me@christine.website>
control/controlclient: minor review feedback fixes
Signed-off-by: Christine Dodrill <me@christine.website>
{control,ipn,systemd}: fix review feedback
Signed-off-by: Christine Dodrill <me@christine.website>
review feedback fixes
Signed-off-by: Christine Dodrill <me@christine.website>
ipn: fix sprintf call
Signed-off-by: Christine Dodrill <me@christine.website>
ipn: make staticcheck less sad
Signed-off-by: Christine Dodrill <me@christine.website>
ipn: print IP address in connected status
Signed-off-by: Christine Dodrill <me@christine.website>
ipn: review feedback
Signed-off-by: Christine Dodrill <me@christine.website>
final fixups
Signed-off-by: Christine Dodrill <me@christine.website>
After mapver 5's incremental netmap updates & user profiles, much of
the remaining bandwidth for streamed MapResponses were redundant,
unchanged PacketFilters. So make MapRequest.Version 6 mean that nil
means unchanged from the previous value.
Cache DNS results of earlier login.tailscale.com control dials, and use
them for future dials if DNS is slow or broken.
Fixes various issues with trickier setups with the domain's DNS server
behind a subnet router.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
When building with redo, also include the git commit hash
from the proprietary repo, so that we have a precise commit
that identifies all build info (including Go toolchain version).
Add a top-level build script demonstrating to downstream distros
how to burn the right information into builds.
Adjust `tailscale version` to print commit hashes when available.
Fixes#841.
Signed-off-by: David Anderson <danderson@tailscale.com>
Also replaces the IPv6Overlay bool with use of DebugFlags, since
it's currently an experimental configuration.
Signed-off-by: David Anderson <danderson@tailscale.com>
Fix from regression in previous commit
(0e3048d8e0) that was caught by e2e
tests.
In that previous commit, the user's own profile was omitted from the
NetworkMap in the case where the user only had one node.
I was going to make support for this advertised from the client, but
turns out only "tailscale status" even uses the UserProfiles field and
fails gracefully (omits that field) if a user profile for a user is
missing, so I think we can just reuse the DeltaPeers field from the
client to ask the control server to also delta encode the user
profiles.
For the few users running 1.1.x (unstable) versions between DeltaPeers
support (1.1.82) and this (~1.1.541), they'll just sometimes have
missing names in "tailscale status --json" or "tailscale status --web"
(the only places the UserProfile is used).
The previous code read too explicitly like log.Printf("I am here1"),
log.Printf("I am here2"). It still is with this change, but prettier, and
less subject to code rearranging order.
On startup, clients do a MapRequest with empty endpoints while they
learn the DERP map to discover the STUN servers they then query to
learn their endpoints.
Set MapRequest.ReadOnly on those initial queries to not broadcast the
empty endpoints out to peers. The read results will come a half second
later (or less).
Updates tailscale/corp#557
Turns out for the particular error I was chasing, it actually returns
200 and zero data. But this code mirrors the same check in the map
poll, and is the right thing to do in the name of future debugging.
Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
DebugForceDisco was a development & safety knob during the the transition
to discovery. It's no longer needed.
Add MapRequest.ReadOnly to prevent clients needing to do two
peer-spamming MapRequest at start-up.
This only adds the field, not the use of the field. (The control server
needs to support it first.)
Updates tailscale/corp#557
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Also remove rebinding logic from the windows router. Magicsock will
instead rebind based on link change signals.
Signed-off-by: David Anderson <danderson@tailscale.com>
control/controlclient, wgengine/filter: extract parsePacketFilter to new constructor in wgengine/filter
Signed-off-by: chungdaniel <daniel@tailscale.com>
Also, bit of behavior change: on non-nil err but expired context,
don't reset the consecutive failure count. I don't think the old
behavior was intentional.
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
NetworkMap text diffs being empty were currently used to short-circuit
calling magicsock's SetNetworkMap (via Engine.SetNetworkMap), but that
went away in c7582dc2 (0.100.0-230)
Prior to c7582dc2 (notably, in 0.100.0-225 and below, down to
0.100.0), a change in only disco key (as when a node restarts) but
without endpoint changes (as would happen for a client not behind a
NAT with random ports) could result in a "netmap diff: (none)" being
printed, as well as Engine.SetNetworkMap being skipped, leading to
broken discovery endpoints.
c7582dc2 fixed the Engine.SetNetworkMap skippage.
This change fixes the "netmap diff: (none)" print so we'll actually see when a peer
restarts with identical endpoints but a new discovery key.
This code is currently racy due to an incorrect assumption
that goal is never modified in-place, so does not require extra locking.
This change makes the assumption correct.
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>