summaryrefslogtreecommitdiffhomepage
path: root/ipn/localapi/localapi.go
AgeCommit message (Collapse)AuthorFilesLines
2025-09-02util/syspolicy: finish adding ts_omit_syspolicy build tags, testsBrad Fitzpatrick1-50/+0
Fixes #16998 Updates #12614 Change-Id: Idf2b1657898111df4be31f356091b2376d0d7f0b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-08-18ipn/localapi: plumb an event bus through the localapi.Handler (#16892)M. J. Fromberger1-3/+22
Some of the operations of the local API need an event bus to correctly instantiate other components (notably including the portmapper). This commit adds that, and as the parameter list is starting to get a bit long and hard to read, I took the opportunity to move the arguments to a config type. Only a few call sites needed to be updated and this API is not intended for general use, so I did not bother to stage the change. Updates #15160 Updates #16842 Change-Id: I7b057d71161bd859f5acb96e2f878a34c85be0ef Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2025-07-21wgengine/magicsock,all: allocate peer relay over disco instead of PeerAPI ↵Jordan Whited1-1/+1
(#16603) Updates tailscale/corp#30583 Updates tailscale/corp#30534 Updates tailscale/corp#30557 Signed-off-by: Dylan Bargatze <dylan@tailscale.com> Signed-off-by: Jordan Whited <jordan@tailscale.com> Co-authored-by: Dylan Bargatze <dylan@tailscale.com>
2025-07-18client/local,cmd/tailscale/cli,ipn/localapi: expose eventbus graph (#16597)Claus Lensbøl1-0/+50
Make it possible to dump the eventbus graph as JSON or DOT to both debug and document what is communicated via the bus. Updates #15160 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2025-07-16ipn/ipnlocal: sort tailscale debug peer-relay-servers slice (#16579)Jordan Whited1-1/+4
Updates tailscale/corp#30036 Signed-off-by: Jordan Whited <jordan@tailscale.com>
2025-07-16cmd/tailscale/cli,ipn/ipnlocal,wgengine/magicsock: implement tailscale debug ↵Jordan Whited1-0/+6
peer-relay-servers (#16577) Updates tailscale/corp#30036 Signed-off-by: Jordan Whited <jordan@tailscale.com>
2025-07-10ipn/localapi: do not break client on event marshalling errors (#16503)Claus Lensbøl1-1/+15
Errors were mashalled without the correct newlines. Also, they could generally be mashalled with more data, so an intermediate was introduced to make them slightly nicer to look at. Updates #15160 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2025-07-08cmd/tailscale/cli,ipn/ipnlocal: restrict logout when AlwaysOn mode is enabledNick Khyl1-1/+1
In this PR, we start passing a LocalAPI actor to (*LocalBackend).Logout to make it subject to the same access check as disconnects made via tailscale down or the GUI. We then update the CLI to allow `tailscale logout` to accept a reason, similar to `tailscale down`. Updates tailscale/corp#26249 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-07-07ipn/{ipnauth,ipnlocal,localapi}: make EditPrefs return an error if changing ↵Nick Khyl1-1/+1
exit node is restricted by policy We extract checkEditPrefsAccessLocked, adjustEditPrefsLocked, and onEditPrefsLocked from the EditPrefs execution path, defining when each step is performed and what behavior is allowed at each stage. Currently, this is primarily used to support Always On mode, to handle the Exit Node enablement toggle, and to report prefs edit metrics. We then use it to enforce Exit Node policy settings by preventing users from setting an exit node and making EditPrefs return an error when an exit node is restricted by policy. This enforcement is also extended to the Exit Node toggle. These changes prepare for supporting Exit Node overrides when permitted by policy and preventing logout while Always On mode is enabled. In the future, implementation of these methods can be delegated to ipnext extensions via the feature hooks. Updates tailscale/corp#29969 Updates tailscale/corp#26249 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-06-18ipn/ipnlocal: make pricing restriction message for Tailnet Lock clearerAnton Tolchanov1-0/+5
Fixes tailscale/corp#24417 Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2025-06-11ipn/localapi,client/local: add debug watcher for bus events (#16239)Claus Lensbøl1-37/+100
Updates: #15160 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2025-06-10cmd/tailscale/cli: add a risk message about rp_filterAnton Tolchanov1-65/+93
We already present a health warning about this, but it is easy to miss on a server when blackholing traffic makes it unreachable. In addition to a health warning, present a risk message when exit node is enabled. Example: ``` $ tailscale up --exit-node=lizard The following issues on your machine will likely make usage of exit nodes impossible: - interface "ens4" has strict reverse-path filtering enabled - interface "tailscale0" has strict reverse-path filtering enabled Please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310 To skip this warning, use --accept-risk=linux-strict-rp-filter $ ``` Updates #3310 Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2025-05-13ipn/ipnlocal: attach Tailnet Lock status to bugreportsAnton Tolchanov1-0/+9
Fixes tailscale/corp#28524 Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2025-05-09health, ipn/ipnlocal: add metrics for various client events (#15828)Jonathan Nobels1-8/+9
updates tailscale/corp#28092 Adds metrics for various client events: * Enabling an exit node * Enabling a mullvad exit node * Enabling a preferred exit node * Setting WantRunning to true/false * Requesting a bug report ID * Profile counts * Profile deletions * Captive portal detection Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2025-04-16feature/taildrop: start moving Taildrop out of LocalBackendBrad Fitzpatrick1-400/+13
This adds a feature/taildrop package, a ts_omit_taildrop build tag, and starts moving code to feature/taildrop. In some cases, code remains where it was but is now behind a build tag. Future changes will move code to an extension and out of LocalBackend, etc. Updates #12614 Change-Id: Idf96c61144d1a5f707039ceb2ff59c99f5c1642f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-04-16net/netmon: publish events to event busDavid Anderson1-1/+4
Updates #15160 Signed-off-by: David Anderson <dave@tailscale.com>
2025-04-16portmapper: update NewClient to use a Config argumentM. J. Fromberger1-11/+17
In preparation for adding more parameters (and later, moving some away), rework the portmapper constructor to accept its arguments on a Config struct rather than positionally. This is a breaking change to the function signature, but one that is very easy to update, and a search of GitHub reveals only six instances of usage outside clones and forks of Tailscale itself, that are not direct copies of the code fixed up here. While we could stub in another constructor, I think it is safe to let those folks do the update in-place, since their usage is already affected by other changes we can't test for anyway. Updates #15160 Change-Id: I9f8a5e12b38885074c98894b7376039261b43f43 Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2025-02-04ipn/{ipnserver,localapi},tsnet: use ipnauth.Self as the actor in tsnet ↵Nick Khyl1-4/+3
localapi handlers With #14843 merged, (*localapi.Handler).servePrefs() now requires a non-nil actor, and other places may soon require it as well. In this PR, we update localapi.NewHandler with a new required parameter for the actor. We then update tsnet to use ipnauth.Self. We also rearrange the code in (*ipnserver.Server).serveHTTP() to pass the actor via Handler's constructor instead of the field. Updates #14823 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31ipn/ipn{auth,server,local}: initial support for the always-on modeNick Khyl1-1/+1
In this PR, we update LocalBackend to set WantRunning=true when applying policy settings to the current profile's prefs, if the "always-on" mode is enabled. We also implement a new (*LocalBackend).EditPrefsAs() method, which is like EditPrefs but accepts an actor (e.g., a LocalAPI client's identity) that initiated the change. If WantRunning is being set to false, the new EditPrefsAs method checks whether the actor has ipnauth.Disconnect access to the profile and propagates an error if they do not. Finally, we update (*ipnserver.actor).CheckProfileAccess to allow a disconnect only if the "always-on" mode is not enabled by the AlwaysOn policy setting. This is not a comprehensive solution to the "always-on" mode across platforms, as instead of disconnecting a user could achieve the same effect by creating a new empty profile, initiating a reauth, or by deleting the profile. These are the things we should address in future PRs. Updates #14823 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-30ipn: generate LoginProfileView and use it instead of *LoginProfile where ↵Nick Khyl1-2/+2
appropriate Conventionally, we use views (e.g., ipn.PrefsView, tailcfg.NodeView, etc.) when dealing with structs that shouldn't be mutated. However, ipn.LoginProfile has been an exception so far, with a mix of passing and returning LoginProfile by reference (allowing accidental mutations) and by value (which is wasteful, given its current size of 192 bytes). In this PR, we generate an ipn.LoginProfileView and use it instead of passing/returning LoginProfiles by mutable reference or copying them when passing/returning by value. Now, LoginProfiles can only be mutated by (*profileManager).setProfilePrefs. Updates #14823 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-24feature/capture: move packet capture to feature/*, out of iOS + CLIBrad Fitzpatrick1-19/+15
We had the debug packet capture code + Lua dissector in the CLI + the iOS app. Now we don't, with tests to lock it in. As a bonus, tailscale.com/net/packet and tailscale.com/net/flowtrack no longer appear in the CLI's binary either. A new build tag ts_omit_capture disables the packet capture code and was added to build_dist.sh's --extra-small mode. Updates #12614 Change-Id: I79b0628c0d59911bd4d510c732284d97b0160f10 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-01-09all: illumos/solaris userspace only supportNahum Shalman1-2/+2
Updates #14565 Change-Id: I743148144938794db0a224873ce76c10dbe6fa5f Signed-off-by: Nahum Shalman <nahamu@gmail.com>
2024-12-31all: add means to set device posture attributes from nodeBrad Fitzpatrick1-0/+28
Updates tailscale/corp#24690 Updates #4077 Change-Id: I05fe799beb1d2a71d1ec3ae08744cc68bcadae2a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-12-04cmd/tailscale,net/netcheck: add debug feature to force preferred DERPJames Tucker1-0/+7
This provides an interface for a user to force a preferred DERP outcome for all future netchecks that will take precedence unless the forced region is unreachable. The option does not persist and will be lost when the daemon restarts. Updates tailscale/corp#18997 Updates tailscale/corp#24755 Signed-off-by: James Tucker <james@tailscale.com>
2024-11-27ipn/localapi: count localapi requests to metric endpointsAnton Tolchanov1-1/+5
Updates tailscale/corp#22075 Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-11-07client/tailscale,ipn/{ipnlocal,localapi}: add a pre-shutdown localAPI ↵Irbe Krumina1-0/+17
endpoint that terminates control connections. (#14028) Adds a /disconnect-control local API endpoint that just shuts down control client. This can be run before shutting down an HA subnet router/app connector replica - it will ensure that all connection to control are dropped and control thus considers this node inactive and tells peers to switch over to another replica. Meanwhile the existing connections keep working (assuming that the replica is given some graceful shutdown period). Updates tailscale/tailscale#14020 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-10-31cmd/tailscale/cli, client/tailscale, ipn/localapi: add tailscale syspolicy ↵Nick Khyl1-0/+50
{list,reload} commands In this PR, we add the tailscale syspolicy command with two subcommands: list, which displays policy settings, and reload, which forces a reload of those settings. We also update the LocalAPI and LocalClient to facilitate these additions. Updates #12687 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-29cmd/tailscale/cli: add 'tailscale metrics' commandAnton Tolchanov1-9/+2
- `tailscale metrics print`: to show metric values in console - `tailscale metrics write`: to write metrics to a file (with a tempfile & rename dance, which is atomic on Unix). Also, remove the `TS_DEBUG_USER_METRICS` envknob as we are getting more confident in these metrics. Updates tailscale/corp#22075 Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-18ipn/{ipnauth,ipnlocal,ipnserver}: send the auth URL to the user who started ↵Nick Khyl1-2/+2
interactive login We add the ClientID() method to the ipnauth.Actor interface and updated ipnserver.actor to implement it. This method returns a unique ID of the connected client if the actor represents one. It helps link a series of interactions initiated by the client, such as when a notification needs to be sent back to a specific session, rather than all active sessions, in response to a certain request. We also add LocalBackend.WatchNotificationsAs and LocalBackend.StartLoginInteractiveAs methods, which are like WatchNotifications and StartLoginInteractive but accept an additional parameter specifying an ipnauth.Actor who initiates the operation. We store these actor identities in watchSession.owner and LocalBackend.authActor, respectively,and implement LocalBackend.sendTo and related helper methods to enable sending notifications to watchSessions associated with actors (or, more broadly, identifiable recipients). We then use the above to change who receives the BrowseToURL notifications: - For user-initiated, interactive logins, the notification is delivered only to the user who initiated the process. If the initiating actor represents a specific connected client, the URL notification is sent back to the same LocalAPI client that called StartLoginInteractive. Otherwise, the notification is sent to all clients connected as that user. Currently, we only differentiate between users on Windows, as it is inherently a multi-user OS. - In all other cases (e.g., node key expiration), we send the notification to all connected users. Updates tailscale/corp#18342 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-09clientupdate, ipn/localapi: don't use google/uuid, thin iOS depsBrad Fitzpatrick1-2/+1
We were using google/uuid in two places and that brought in database/sql/driver. We didn't need it in either place. Updates #13760 Updates tailscale/corp#20099 Change-Id: Ieed32f1bebe35d35f47ec5a2a429268f24f11f1f Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-09-25util/usermetrics: make usermetrics non-globalKristoffer Dalby1-2/+1
this commit changes usermetrics to be non-global, this is a building block for correct metrics if a go process runs multiple tsnets or in tests. Updates #13420 Updates tailscale/corp#22075 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-24cli: add `tailscale dns query` (#13368)Andrea Gottardo1-0/+46
Updates tailscale/tailscale#13326 Adds a CLI subcommand to perform DNS queries using the internal DNS forwarder and observe its internals (namely, which upstream resolvers are being used). Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-09-04cli: implement `tailscale dns status` (#13353)Andrea Gottardo1-0/+39
Updates tailscale/tailscale#13326 This PR begins implementing a `tailscale dns` command group in the Tailscale CLI. It provides an initial implementation of `tailscale dns status` which dumps the state of the internal DNS forwarder. Two new endpoints were added in LocalAPI to support the CLI functionality: - `/netmap`: dumps a copy of the last received network map (because the CLI shouldn't have to listen to the ipn bus for a copy) - `/dns-osconfig`: dumps the OS DNS configuration (this will be very handy for the UI clients as well, as they currently do not display this information) My plan is to implement other subcommands mentioned in tailscale/tailscale#13326, such as `query`, in later PRs. Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-08-28ipn/{ipnauth,ipnlocal,ipnserver,localapi}: start baby step toward moving ↵Nick Khyl1-108/+4
access checks from the localapi.Handler to the LocalBackend Currently, we use PermitRead/PermitWrite/PermitCert permission flags to determine which operations are allowed for a LocalAPI client. These checks are performed when localapi.Handler handles a request. Additionally, certain operations (e.g., changing the serve config) requires the connected user to be a local admin. This approach is inherently racey and is subject to TOCTOU issues. We consider it to be more critical on Windows environments, which are inherently multi-user, and therefore we prevent more than one OS user from connecting and utilizing the LocalBackend at the same time. However, the same type of issues is also applicable to other platforms when switching between profiles that have different OperatorUser values in ipn.Prefs. We'd like to allow more than one Windows user to connect, but limit what they can see and do based on their access rights on the device (e.g., an local admin or not) and to the currently active LoginProfile (e.g., owner/operator or not), while preventing TOCTOU issues on Windows and other platforms. Therefore, we'd like to pass an actor from the LocalAPI to the LocalBackend to represent the user performing the operation. The LocalBackend, or the profileManager down the line, will then check the actor's access rights to perform a given operation on the device and against the current (and/or the target) profile. This PR does not change the current permission model in any way, but it introduces the concept of an actor and includes some preparatory work to pass it around. Temporarily, the ipnauth.Actor interface has methods like IsLocalSystem and IsLocalAdmin, which are only relevant to the current permission model. It also lacks methods that will actually be used in the new model. We'll be adding these gradually in the next PRs and removing the deprecated methods and the Permit* flags at the end of the transition. Updates tailscale/corp#18342 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-27usermetric: add initial user-facing metricsKristoffer Dalby1-0/+15
This commit adds a new usermetric package and wires up metrics across the tailscale client. Updates tailscale/corp#22075 Co-authored-by: Anton Tolchanov <anton@tailscale.com> Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-08-14ipn/localapi: only flush relevant data in multiFilePostResponseWriter.Flush()Percy Wegmann1-4/+11
This prevents two things: 1. Crashing if there's no response body 2. Sending a nonsensical 0 response status code Updates tailscale/corp#22357 Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-06-18proxymap, various: distinguish between different protocolsAndrew Dunham1-2/+2
Previously, we were registering TCP and UDP connections in the same map, which could result in erroneously removing a mapping if one of the two connections completes while the other one is still active. Add a "proto string" argument to these functions to avoid this. Additionally, take the "proto" argument in LocalAPI, and plumb that through from the CLI and add a new LocalClient method. Updates tailscale/corp#20600 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I35d5efaefdfbf4721e315b8ca123f0c8af9125fb
2024-06-14ipn/localapi,client/tailscale,cmd/derper: add WhoIs lookup by nodekey, use ↵Brad Fitzpatrick1-2/+17
in derper Fixes #12465 Change-Id: I9b7c87315a3d2b2ecae2b8db9e94b4f5a1eef74a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-06-10ipn/{ipnlocal,localapi},net/netkernelconf,client/tailscale,cmd/containerboot ↵Irbe Krumina1-0/+18
: optionally enable UDP GRO forwarding for containers (#12410) Add a new TS_EXPERIMENTAL_ENABLE_FORWARDING_OPTIMIZATIONS env var that can be set for tailscale/tailscale container running as a subnet router or exit node to enable UDP GRO forwarding for improved performance. See https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes This is currently considered an experimental approach; the configuration support is partially to allow further experimentation with containerized environments to evaluate the performance improvements. Updates tailscale/tailscale#12295 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-06-02tsnet,wgengine/netstack: add ListenPacket and testsMaisem Ali1-1/+4
This adds a new ListenPacket function on tsnet.Server which acts mostly like `net.ListenPacket`. Unlike `Server.Listen`, this requires listening on a specific IP and does not automatically listen on both V4 and V6 addresses of the Server when the IP is unspecified. To test this, it also adds UDP support to tsdial.Dialer.UserDial and plumbs it through the localapi. Then an associated test to make sure the UDP functionality works from both sides. Updates #12182 Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-04-29appc: setting AdvertiseRoutes explicitly discards app connector routesFran Bull1-0/+6
This fixes bugs where after using the cli to set AdvertiseRoutes users were finding that they had to restart tailscaled before the app connector would advertise previously learned routes again. And seems more in line with user expectations. Fixes #11006 Signed-off-by: Fran Bull <fran@tailscale.com>
2024-04-26ipn/{ipnlocal,localapi},wgengine{,/magicsock}: plumb health.TrackerBrad Fitzpatrick1-6/+4
Down to 25 health.Global users. After this remains controlclient & net/dns & wgengine/router. Updates #11874 Updates #4136 Change-Id: I6dd1856e3d9bf523bdd44b60fb3b8f7501d5dc0d Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-25health: add Tracker type, in prep for removing global variablesBrad Fitzpatrick1-1/+1
This moves most of the health package global variables to a new `health.Tracker` type. But then rather than plumbing the Tracker in tsd.System everywhere, this only goes halfway and makes one new global Tracker (`health.Global`) that all the existing callers now use. A future change will eliminate that global. Updates #11874 Updates #4136 Change-Id: I6ee27e0b2e35f68cb38fecdb3b2dc4c3f2e09d68 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-22ipn/local: log OS-specific diagnostic information as JSON (#11700)Joe Tsai1-1/+1
There is an undocumented 16KiB limit for text log messages. However, the limit for JSON messages is 256KiB. Even worse, logging JSON as text results in significant overhead since each double quote needs to be escaped. Instead, use logger.Logf.JSON to explicitly log the info as JSON. We also modify osdiag to return the information as structured data rather than implicitly have the package log on our behalf. This gives more control to the caller on how to log. Updates #7802 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-16all: use Go 1.22 range-over-intBrad Fitzpatrick1-1/+1
Updates #11058 Change-Id: I35e7ef9b90e83cac04ca93fd964ad00ed5b48430 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15ipn/ipnlocal: make StartLoginInteractive take (yet unused) contextBrad Fitzpatrick1-1/+1
In prep for future fix to undermentioned issue. Updates tailscale/tailscale#7036 Change-Id: Ide114db917dcba43719482ffded6a9a54630d99e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15cmd/tailscale, ipn/ipnlocal: add suggest exit node CLI option (#11407)Claire Wang1-0/+16
Updates tailscale/corp#17516 Signed-off-by: Claire Wang <claire@tailscale.com>
2024-04-14ipn/ipnlocal: remove some dead code (legacyBackend methods) from LocalBackendBrad Fitzpatrick1-14/+0
Nothing used it. Updates #11649 Change-Id: Ic1c331d947974cd7d4738ff3aafe9c498853689e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-07ipn/{ipnlocal,localapi}: add API to toggle use of exit nodeBrad Fitzpatrick1-0/+27
This is primarily for GUIs, so they don't need to remember the most recently used exit node themselves. This adds some CLI commands, but they're disabled and behind the WIP envknob, as we need to consider naming (on/off is ambiguous with running an exit node, etc) as well as automatic exit node selection in the future. For now the CLI commands are effectively developer debug things to test the LocalAPI. Updates tailscale/corp#18724 Change-Id: I9a32b00e3ffbf5b29bfdcad996a4296b5e37be7e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-05ipn/localapi: sort localapi handler map keysBrad Fitzpatrick1-20/+20
Updates #cleanup Change-Id: I750ed8d033954f1f8786fb35dd16895bb1c5af8e Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>