The new Android app and its libtailscale don't use this anymore;
it uses LocalAPI like other clients now.
Updates #11649
Change-Id: Ic9f42b41e0e0280b82294329093dc6c275f41d50
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
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>
This names the func() that Once-unlocked LocalBackend.mu. It does so
both for docs and because it can then have a method: Unlock, for the
few points that need to explicitly unlock early (the cause of all this
mess). This makes those ugly points easy to find, and also can then
make them stricter, panicking if the mutex is already unlocked. So a
normal call to the func just once-releases the mutex, returning false
if it's already done, but the Unlock method is the strict one.
Then this uses it more, so most the b.mu.Unlock calls remaining are
simple cases and usually defers.
Updates #11649
Change-Id: Ia070db66c54a55e59d2f76fdc26316abf0dd4627
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
A number of methods in LocalBackend (with suffixed "LockedOnEntry")
require b.mu be held but unlock it on the way out. That's asymmetric
and atypical and error prone.
This adds a helper method to LocalBackend that locks the mutex and
returns a sync.OnceFunc that unlocks the mutex. Then we pass around
that unlocker func down the chain to make it explicit (and somewhat
type check the passing of ownership) but also let the caller defer
unlock it, in the case of errors/panics that happen before the callee
gets around to calling the unlock.
This revealed a latent bug in LocalBackend.DeleteProfile which double
unlocked the mutex.
Updates #11649
Change-Id: I002f77567973bd77b8906bfa4ec9a2049b89836a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This change makes the normalizeShareName function public, so it can be
used for validation in control.
Updates tailscale/corp#16827
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
This change switches the api to /drive, rather than the previous /tailfs
as well as updates the log lines to reflect the new value. It also
cleans up some existing tailfs references.
Updates tailscale/corp#16827
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
This change updates all tailfs functions and the majority of the tailfs
variables to use the new drive naming.
Updates tailscale/corp#16827
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
This change updates the tailfs file and package names to their new
naming convention.
Updates #tailscale/corp#16827
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
We're tracking down a new instance of memory usage, and excessive memory usage
from sockstats is definitely not going to help with debugging, so disable it by
default on mobile.
Updates tailscale/corp#18514
Signed-off-by: James Tucker <james@tailscale.com>
This allows sending multiple files via Taildrop in one request.
Progress is tracked via ipn.Notify.
Updates tailscale/corp#18202
Signed-off-by: Percy Wegmann <percy@tailscale.com>
This allows sending multiple files via Taildrop in one request.
Progress is tracked via ipn.Notify.
Updates tailscale/corp#18202
Signed-off-by: Percy Wegmann <percy@tailscale.com>
For example, if we get a 404 when downloading a file, we'll report access.
Also, to reduce verbosty of logs, this elides 0 length files.
Updates tailscale/corp#17818
Signed-off-by: Percy Wegmann <percy@tailscale.com>
This change introduces some basic logging into the access and share
pathways for tailfs.
Updates tailscale/corp#17818
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
First we had Capabilities []string. Then
https://tailscale.com/blog/acl-grants (#4217) brought CapMap, a
superset of Capabilities. Except we never really finished the
transition inside the codebase to go all-in on CapMap. This does so.
Notably, this coverts Capabilities on the wire early to CapMap
internally so the code can only deal in CapMap, even against an old
control server.
In the process, this removes PeerChange.Capabilities support, which no
known control plane sent anyway. They can and should use
PeerChange.CapMap instead.
Updates #11508
Updates #4217
Change-Id: I872074e226b873f9a578d9603897b831d50b25d9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
When node attributes were super rare, the O(n) slice scans looking for
node attributes was more acceptable. But now more code and more users
are using increasingly more node attributes. Time to make it a map.
Noticed while working on tailscale/corp#17879
Updates #cleanup
Change-Id: Ic17c80341f418421002fbceb47490729048756d2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This allows the UI to distinguish between 'no shares' versus
'not being notified about shares'.
Updates ENG-2843
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Fix a bug where all proxies got configured with --accept-routes set to true.
The bug was introduced in https://github.com/tailscale/tailscale/pull/11238.
Updates#cleanup
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Enable the web client over 100.100.100.100 by default. Accepting traffic
from [tailnet IP]:5252 still requires setting the `webclient` user pref.
Updates https://github.com/tailscale/tailscale/issues/10261
Signed-off-by: Mario Minardi <mario@tailscale.com>
Add a disable-web-client node attribute and add handling for disabling
the web client when this node attribute is set.
Updates https://github.com/tailscale/tailscale/issues/10261
Signed-off-by: Mario Minardi <mario@tailscale.com>
If the client uses the default Tailscale control URL, validate that all
PopBrowserURLs are under tailscale.com or *.tailscale.com. This reduces
the risk of a compromised control plane opening phishing pages for
example.
The client trusts control for many other things, but this is one easy
way to reduce that trust a bit.
Fixes#11393
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
To force the problem in its worst case scenario before fixing it.
Updates tailscale/corp#17859
Change-Id: I2c8b8e5f15c7801e1ab093feeafac52ec175a763
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
- Updates API to support renaming TailFS shares.
- Adds a CLI rename subcommand for renaming a share.
- Renames the CLI subcommand 'add' to 'set' to make it clear that
this is an add or update.
- Adds a unit test for TailFS in ipnlocal
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Previously, the configuration of which folders to share persisted across
profile changes. Now, it is tied to the user's profile.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
This eliminates unnecessary map.Clone() calls and also eliminates
repetitive notifications about the same set of shares.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Updates tailscale/corp#17859
Provides a local API endpoint to be called from the GUI to inform the backend when the client menu is opened or closed.
cc @bradfitz
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
This is a temporary solution to at least omit Mullvad exit nodes
from the list of TailFS peers. Once we can identify peers that are
actually sharing via TailFS, we can remove this, but for alpha it'll
be sufficient to just omit Mullvad.
Updates tailscale/corp#17766
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Moving logic that manipulates a ServeConfig into recievers on the
ServeConfig in the ipn package. This is setup work to allow the
web client and cli to both utilize these shared functions to edit
the serve config.
Any logic specific to flag parsing or validation is left untouched
in the cli command. The web client will similarly manage its
validation of user's requested changes. If validation logic becomes
similar-enough, we can make a serve util for shared functionality,
which likely does not make sense in ipn.
Updates #10261
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
In preparation for changes to allow configuration of serve/funnel
from the web client, this commit moves some functionality that will
be shared between the CLI and web client to the ipn package's
serve.go file, where some other util funcs are already defined.
Updates #10261
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This allows the Mac application to regain access to restricted
folders after restarts.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
We used a HandleSet before when we didn't have a unique handle. But a
sessionID is a unique handle, so use that instead. Then that replaces
the other map we had.
And now we'll have a way to look up an IPN session by sessionID for
later.
Updates tailscale/corp#17859
Change-Id: I5f647f367563ec8783c643e49f93817b341d9064
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This fixes a regression introduced with 993acf4 and released in
v1.60.0.
The regression caused us to intercept all userspace traffic to port
8080 which prevented users from exposing their own services to their
tailnet at port 8080.
Now, we only intercept traffic to port 8080 if it's bound for
100.100.100.100 or fd7a:115c:a1e0::53.
Fixes#11283
Signed-off-by: Percy Wegmann <percy@tailscale.com>
(cherry picked from commit 17cd0626f3)
This adds a method to wgengine.Engine and plumbed down into magicsock
to add a way to get a type-safe Tailscale-safe wrapper around a
wireguard-go device.Peer that only exposes methods that are safe for
Tailscale to use internally.
It also removes HandshakeAttempts from PeerStatusLite that was just
added as it wasn't needed yet and is now accessible ala cart as needed
from the Peer type accessor.
None of this is used yet.
Updates #7617
Change-Id: I07be0c4e6679883e6eeddf8dbed7394c9e79c5f4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
... rather than 1970. Code was using IsZero against the 1970 team
(which isn't a zero value), but fortunately not anywhere that seems to
have mattered.
Updates #cleanup
Change-Id: I708a3f2a9398aaaedc9503678b4a8a311e0e019e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Not yet used. This is being made available so magicsock/wgengine can
use it to ignore certain sends (UDP + DERP) later on at least mobile,
letting wireguard-go think it's doing its full attempt schedule, but
we can cut it short conditionally based on what we know from the
control plane.
Updates #7617
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia367cf6bd87b2aeedd3c6f4989528acdb6773ca7
When reverse path filtering is in strict mode on Linux, using an exit
node blocks all network connectivity. This change adds a warning about
this to `tailscale status` and the logs.
Example in `tailscale status`:
```
- not connected to home DERP region 22
- The following issues on your machine will likely make usage of exit nodes impossible: [interface "eth0" has strict reverse-path filtering enabled], please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310
```
Example in the logs:
```
2024/02/21 21:17:07 health("overall"): error: multiple errors:
not in map poll
The following issues on your machine will likely make usage of exit nodes impossible: [interface "eth0" has strict reverse-path filtering enabled], please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310
```
Updates #3310
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
Tailscaled becomes inoperative if the Tailscale Tunnel wintun adapter is abruptly removed.
wireguard-go closes the device in case of a read error, but tailscaled keeps running.
This adds detection of a closed WireGuard device, triggering a graceful shutdown of tailscaled.
It is then restarted by the tailscaled watchdog service process.
Fixes#11222
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Small fix to make sure doctor API endpoint returns correctly - I spotted it when checking my tailscaled node and noticed it was handled slightly different compare to the rest
Signed-off-by: San <santrancisco@users.noreply.github.com>
FileSystemForLocal was listening on the node's Tailscale address,
which potentially exposes the user's view of TailFS shares to other
Tailnet users. Remote nodes should connect to exported shares via
the peerapi.
This removes that code so that FileSystemForLocal is only avaialable
on 100.100.100.100:8080.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Adds support for node attribute tailfs:access. If this attribute is
not present, Tailscale will not accept connections to the local TailFS
server at 100.100.100.100:8080.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Add a WebDAV-based folder sharing mechanism that is exposed to local clients at
100.100.100.100:8080 and to remote peers via a new peerapi endpoint at
/v0/tailfs.
Add the ability to manage folder sharing via the new 'share' CLI sub-command.
Updates tailscale/corp#16827
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Fixestailscale/support-escalations#23.
authURLs returned by control expire after 1 hour from creation. Customer reported that the Tailscale client on macOS would sending users to a stale authentication page when clicking on the `Login...` menu item. This can happen when clicking on Login after leaving the device unattended for several days. The device key expires, leading to the creation of a new authURL, however the client doesn't keep track of when the authURL was created. Meaning that `login-interactive` would send the user to an authURL that had expired server-side a long time before.
This PR ensures that whenever `login-interactive` is called via LocalAPI, an authURL that is too old won't be used. We force control to give us a new authURL whenever it's been more than 30 minutes since the last authURL was sent down from control.
Apply suggestions from code review
Set interval to 6 days and 23 hours
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
If an app connector is also configured as an exit node, it should still
advertise discovered routes that are not covered by advertised routes,
excluding the exit node routes.
Updates tailscale/corp#16928
Signed-off-by: James Tucker <james@tailscale.com>
If any domain along a CNAME chain matches any of the routed domains, add
routes for the discovered domains.
Fixestailscale/corp#16928
Signed-off-by: James Tucker <james@tailscale.com>
The API on the DNS record parser is slightly subtle and requires
explicit handling of unhandled records. Failure to advance previously
resulted in an infinite loop in the pretty responder for any reply that
contains a record other than A/AAAA/TXT.
Updates tailscale/corp#16928
Signed-off-by: James Tucker <james@tailscale.com>
When reporting ssh host keys to control, log a warning
if we're unable to get the SSH host keys.
Updates tailscale/escalations#21
Signed-off-by: Percy Wegmann <percy@tailscale.com>
Don't append a trailing slash to a request path
to the reverse proxy that matches the mount point exactly.
Updates tailscale/tailscale#10730
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit implements probing of UDP path lifetime on the tail end of
an active direct connection. Probing configuration has two parts -
Cliffs, which are various timeout cliffs of interest, and
CycleCanStartEvery, which limits how often a probing cycle can start,
per-endpoint. Initially a statically defined default configuration will
be used. The default configuration has cliffs of 10s, 30s, and 60s,
with a CycleCanStartEvery of 24h. Probing results are communicated via
clientmetric counters. Probing is off by default, and can be enabled
via control knob. Probing is purely informational and does not yet
drive any magicsock behaviors.
Updates #540
Signed-off-by: Jordan Whited <jordan@tailscale.com>
This change allows us to perform batch modification for new route
advertisements and route removals. Additionally, we now handle the case
where newly added routes are covered by existing ranges.
This change also introduces a new appctest package that contains some
shared functions used for testing.
Updates tailscale/corp#16833
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
If there are routes changes as a side effect of an app connector
configuration update, the connector configuration may want to reenter a
lock, so must be started asynchronously.
Updates tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
Control can now send down a set of routes along with the domains, and
the routes will be advertised, with any newly overlapped routes being
removed to reduce the size of the routing table.
Fixestailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
Also perform minor cleanups on the ctxkey package itself.
Provide guidance on when to use ctxkey.Key[T] over ctxkey.New.
Also, allow for interface kinds because the value wrapping trick
also happens to fix edge cases with interfaces in Go.
Updates #cleanup
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
When establishing connections to the ipnserver, we validate that the
local user is allowed to connect. If Tailscale is currently being
managed by a different user (primarily for multi-user Windows installs),
we don't allow the connection.
With the new device web UI, the inbound connection is coming from
tailscaled itself, which is often running as "NT AUTHORITY\SYSTEM".
In this case, we still want to allow the connection, even though it
doesn't match the user running the Tailscale GUI. The SYSTEM user has
full access to everything on the system anyway, so this doesn't escalate
privileges.
Eventually, we want the device web UI to run outside of the tailscaled
process, at which point this exception would probably not be needed.
Updates tailscale/corp#16393
Signed-off-by: Will Norris <will@tailscale.com>
Removes the avoidFinalRename logic and all associated code as it is no longer required by the Apple clients.
Enables resume logic to be usable for Apple clients.
Fixestailscale/corp#14772
Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
Unlike most prefs, the ControlURL policy needs to take effect before
login. This resolves an issue where on first start, even when the
ControlURL policy is set, it will generate a login URL to the Tailscale
SaaS server.
Updates tailscale/coral#118
Fixes#10736
Change-Id: I6da2a521f64028c15dbb6ac8175839fc3cc4e858
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
This command allows observing whether a given dialer ("SystemDial",
"UserDial", etc.) will successfully obtain a connection to a provided
host, from inside tailscaled itself. This is intended to help debug a
variety of issues from subnet routers to split DNS setups.
Updates #9619
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie01ebb5469d3e287eac633ff656783960f697b84
Using reflect.MethodByName disables some linked deadcode optimizations
and makes our binaries much bigger.
Difference before/after this commit:
```
-rwxr-xr-x 1 awly awly 30M Dec 19 15:28 tailscaled.after*
-rwxr-xr-x 1 awly awly 43M Dec 19 15:27 tailscaled.before*
```
Fixes#10627
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Individual route advertisements that are covered by existing routes are
no longer advertised. If an upstream returns 0.0.0.0, 127.x, and other
common unwanted addresses those are also rejected.
Updates #16425
Signed-off-by: James Tucker <james@tailscale.com>
The cmpx.Compare function (and associated interface) are now available
in the standard library as cmp.Compare. Remove our version of it and use
the version from the standard library.
Updates #cleanup
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4be3ac63d466c05eb7a0babb25cb0d41816fbd53
When auto-update setting in local Prefs is unset, apply the tailnet
default value from control. This only happens once, when we apply the
default (or when the user manually overrides it), tailnet default no
longer affects the node.
Updates #16244
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Previously, the "RunExitNode" policy merely controlled the visibility of
the "run as exit node" menu item, not the setting itself. This migrates
that setting to a preference option named "AdvertiseExitNode".
Updates ENG-2138
Change-Id: Ia6a125beb6b4563d380c6162637ce4088f1117a0
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Some fields if `ipn.Prefs` are structs. `ipn.MaskedPrefs` has a single
level of boolean `*Set` flags, which doesn't map well to nested structs
within `ipn.Prefs`.
Change `MaskedPrefs` and `ApplyEdits` to support `FooSet` struct fields
that map to a nested struct of `ipn.Prefs` like `AutoUpdates`. Each
struct field in `MaskedPrefs` is just a bundle of more `Set` bool fields
or other structs. This allows you to have a `Set` flag for any
arbitrarily-nested field of `ipn.Prefs`.
Also, make `ApplyEdits` match fields between `Prefs` and `MaskedPrefs`
by name instead of order, to make it a bit less finicky. It's probably
slower but `ipn.ApplyEdits` should not be in any hot path.
As a result, `AutoUpdate.Check` and `AutoUpdate.Apply` fields don't
clobber each other when set individually.
Updates #16247
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Due to the Sparkle preference naming convention, macsys already has a
policy key named "ApplyUpdates" that merely shows or hides the menu
item that controls if auto updates are installed, rather than directly
controlling the setting.
For other platforms, we are going to use "InstallUpdates" instead
because it seemed better than the other options that were considered.
Updates ENG-2127
Updates tailscale/corp#16247
Change-Id: Ia6a125beb6b4563d380c6162637ce4088f1117a0
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
This adds support for enforcing exit node LAN access, DNS and subnet
routes.
Adding new preference policies was getting repetitive, so this turns
some of the boilerplate into a table.
Updates tailscale/corp#15585
Updates ENG-2240
Change-Id: Iabd3c42b0ae120b3145fac066c5caa7fc4d67824
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Previously, policies affected the default prefs for a new profile, but
that does not affect existing profiles. This change ensures that
policies are applied whenever preferences are loaded or changed, so a
CLI or GUI client that does not respect the policies will still be
overridden.
Exit node IP is dropped from this PR as it was implemented elsewhere
in #10172.
Fixestailscale/corp#15585
Change-Id: Ide4c3a4b00a64e43f506fa1fab70ef591407663f
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Adds policy keys ExitNodeID and ExitNodeIP.
Uses the policy keys to determine the exit node in preferences.
Fixestailscale/corp#15683
Signed-off-by: Claire Wang <claire@tailscale.com>
Adds AllowedIPs to PeerStatus, allowing for easier lookup of the
routes allowed to be routed to a node. Will be using the AllowedIPs
of the self node from the web client interface to display approval
status of advertised routes.
Updates #10261
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
To be consistent with the formatting of other warnings, pass available
update health message instead of handling ClientVersion in he CLI.
Fixes#10312
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This PR starts to persist the NetMap tailnet name in SetPrefs so that tailscaled
clients can use this value to disambiguate fast user switching from one tailnet
to another that are under the same exact login. We will also try to backfill
this information during backend starts and profile switches so that users don't
have to re-authenticate their profile. The first client to use this new
information is the CLI in 'tailscale switch -list' which now uses text/tabwriter
to display the ID, Tailnet, and Account. Since account names are ambiguous, we
allow the user to pass 'tailscale switch ID' to specify the exact tailnet they
want to switch to.
Updates #9286
Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
This takes advantage of existing functionality in ipn/ipnlocal to adjust
the local clock based on periodic time signals from the control server.
This way, when checking things like SSHRule expirations, calculations are
protected incorrectly set local clocks.
Fixestailscale/corp#15796
Signed-off-by: Percy Wegmann <percy@tailscale.com>
This change removes the existing debug-web-client localapi endpoint
and replaces it with functions passed directly to the web.ServerOpts
when constructing a web.ManageServerMode client.
The debug-web-client endpoint previously handled making noise
requests to the control server via the /machine/webclient/ endpoints.
The noise requests must be made from tailscaled, which has the noise
connection open. But, now that the full client is served from
tailscaled, we no longer need to proxy this request over the localapi.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
So the control plane can delete TXT records more aggressively
after client's done with ACME fetch.
Updates tailscale/corp#15848
Change-Id: I4f1140305bee11ee3eee93d4fec3aef2bd6c5a7e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Previously we would return the full error from Stat or Open, possibily exposing the full file path. This change will log the error and return the generic error message "an error occurred reading the file or directory".
Updates tailscale/corp#15485
Signed-off-by: Tyler Smalley <tyler@tailscale.com>
In DERP homeless mode, a DERP home connection is not sought or
maintained and the local node is not reachable.
Updates #3363
Updates tailscale/corp#396
Change-Id: Ibc30488ac2e3cfe4810733b96c2c9f10a51b8331
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Adds a new sync.Mutex field to the webClient struct, rather than
using the general LocalBackend mutex. Since webClientGetOrInit
(previously WebClientInit) gets called on every connection, we
want to avoid holding the lock on LocalBackend just to check if
the server is initialized.
Moves all web_client.go funcs over to using the webClient.mu field.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
For consistency with the "WebClient" naming of the other functions
here. Also fixed a doc typo.
A #cleanup
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Now that 1.54 has released, and the new web client will be included in
1.56, we can remove the need for the node capability. This means that
all 1.55 unstable builds, and then eventually the 1.56 build, will work
without setting the node capability.
The web client still requires the "webclient" user pref, so this does
NOT mean that the web client will be on by default for all devices.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
return early if handler is nil. Go ahead and return the error from
handler, though in this case the caller isn't doing anything with it
(which has always been the case).
Updates #10177
Updates #10251
Signed-off-by: Will Norris <will@tailscale.com>
Tailscale serve maintains a set of listeners so that serve traffic from
the local device can be properly served when running in kernel
networking mode. #10177 refactored that logic so that it could be reused
by the internal web client as well. However, in my refactoring I missed
actually calling the serve handler to handle the traffic.
Updates #10177
Signed-off-by: Will Norris <will@tailscale.com>
This prevents a panic in some cases where WebClientShutdown is called
multiple times.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
When we run tailscled under systemd, restarting the unit kills all child
processes, including "tailscale update". And during update, the package
manager will restart the tailscaled unit. Specifically on Debian-based
distros, interrupting `apt-get install` can get the system into a wedged
state which requires the user to manually run `dpkg --configure` to
recover.
To avoid all this, use `systemd-run` where available to run the
`tailscale update` process. This launches it in a separate temporary
unit and doesn't kill it when parent unit is restarted.
Also, detect when `apt-get install` complains about aborted update and
try to restore the system by running `dpkg --configure tailscale`. This
could help if the system unexpectedly shuts down during our auto-update.
Fixes https://github.com/tailscale/corp/issues/15771
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Simply reading the taildrop directory can pop up security dialogs
on platforms like macOS. Avoid this by only performing garbage collection
of partial and deleted files after the first received taildrop file,
which would have prompted the security dialog window.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This change exposes SilentDisco as a control knob, and plumbs it down to
magicsock.endpoint. No changes are being made to magicsock.endpoint
disco behavior, yet.
Updates #540
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Use the `qpkg_cli` to check for updates and install them. There are a
couple special things about this compare to other updaters:
* qpkg_cli can tell you when upgrade is available, but not what the
version is
* qpkg_cli --add Tailscale works for new installs, upgrades and
reinstalling existing version; even reinstall of existing version
takes a while
Updates #10178
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Some conditional paths may otherwise skip the hostinfo update, so kick
it off asynchronously as other code paths do.
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
On unix systems, the check involves executing sudo, which is slow.
Instead of doing it for every incoming request, move the logic into
localapi serveServeConfig handler and do it as needed.
Updates tailscale/corp#15405
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
The local web client has the same characteristic as tailscale serve, in
that it needs a local listener to allow for connections from the local
machine itself when running in kernel networking mode.
This change renames and adapts the existing serveListener to allow it to
be used by the web client as well.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
The c2n part was broken because we were not looking up the tailscale
binary for that GOOS. The rest of the update was failing at the `pkg
upgrade` confirmation prompt. We also need to manually restart
tailscaled after update.
Updates #cleanup
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
As part of tailnet-lock netmap processing, the LocalBackend mutex
is unlocked so we can potentially make a network call. Its possible
(during shutdown or while the control client is being reset) for
b.cc to become nil before the lock is picked up again.
Fixes: #9554
Signed-off-by: Tom DNetto <tom@tailscale.com>
App connectors handle DNS requests for app domains over PeerAPI,
but a safety check verifies the requesting peer has at least permission
to send traffic to 0.0.0.0:53 (or 2000:: for IPv6) before handling the DNS
request. The correct filter rules are synthesized by the coordination server
and sent down, but the address needs to be part of the 'local net' for the
filter package to even bother checking the filter rules, so we set them here.
See: https://github.com/tailscale/corp/issues/11961 for more information.
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: ENG-2405
This change introduces a c2n endpoint that returns a map of domains to a
slice of resolved IP addresses for the domain.
Fixestailscale/corp#15657
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
For an operator user, require them to be able to `sudo tailscale` to use
`tailscale serve`. This is similar to the Windows elevated token check.
Updates tailscale/corp#15405
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Require that requests to servers in manage mode are made to the
Tailscale IP (either ipv4 or ipv6) or quad-100. Also set various
security headers on those responses. These might be too restrictive,
but we can relax them as needed.
Allow requests to /ok (even in manage mode) with no checks. This will be
used for the connectivity check from a login client to see if the
management client is reachable.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
(*Token).IsAdministrator is supposed to return true even when the user is
running with a UAC limited token. The idea is that, for the purposes of
this check, we don't care whether the user is *currently* running with
full Admin rights, we just want to know whether the user can
*potentially* do so.
We accomplish this by querying for the token's "linked token," which
should be the fully-elevated variant, and checking its group memberships.
We also switch ipn/ipnserver/(*Server).connIsLocalAdmin to use the elevation
check to preserve those semantics for tailscale serve; I want the
IsAdministrator check to be used for less sensitive things like toggling
auto-update on and off.
Fixes#10036
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
if the user pref and nodecap for the new web client are enabled, serve
the client over requests to 100.100.100.100. Today, that is just a
static page that lists the local Tailcale IP addresses.
For now, this will render the readonly full management client, with an
"access" button that sends the user through check mode. After
completing check mode, they will still be in the read-only view, since
they are not accessing the client over Tailscale.
Instead, quad100 should serve the lobby client that has a "manage"
button that will open the management client on the Tailscale IP (and
trigger check mode). That is something we'll fix in a subsequent PR in
the web client code itself.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
Avoids the need to pipe a web client dev flag through the tailscaled
command.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
The existing read permission check looks like an oversight. Write seems
more appropriate for sining new nodes.
Updates https://github.com/tailscale/corp/issues/15506
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Adds a new Mode to the web server, indicating the specific
scenario the constructed server is intended to be run in. Also
starts filling this from the cli/web and ipn/ipnlocal callers.
From cli/web this gets filled conditionally based on whether the
preview web client node cap is set. If not set, the existing
"legacy" client is served. If set, both a login/lobby and full
management client are started (in "login" and "manage" modes
respectively).
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
The design changed during integration and testing, resulting in the
earlier implementation growing in the appc package to be intended now
only for the sniproxy implementation. That code is moved to it's final
location, and the current App Connector code is now renamed.
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
Now uses webClientAtomicBool as the source of truth for whether the web
client should be running in tailscaled, with it updated when either the
RunWebClient pref or CapabilityPreviewWebClient node capability changes.
This avoids requiring holding the LocalBackend lock on each call to
ShouldRunWebClient to check for the CapabilityPreviewWebClient value.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Clients optionally request private key filtering. If they don't, we
should require Write access for the user.
Updates https://github.com/tailscale/corp/issues/15506
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
The AppConnector is now configured by the mapcap from the control plane.
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
An EmbeddedAppConnector is added that when configured observes DNS
responses from the PeerAPI. If a response is found matching a configured
domain, routes are advertised when necessary.
The wiring from a configuration in the netmap capmap is not yet done, so
while the connector can be enabled, no domains can yet be added.
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
WebClientShutdown tries to acquire the b.mu lock, so run it in a go
routine so that it can finish shutdown after setPrefsLockedOnEntry is
finished. This is the same reason b.sshServer.Shutdown is run in a go
routine.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
For consistency and clarity around what the LocalBackend.web field
is used for.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
instead of starting a separate server listening on a particular port,
use the TCPHandlerForDst method to intercept requests for the special
web client port (currently 5252, probably configurable later).
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
This is not currently exposed as a user-settable preference through
`tailscale up` or `tailscale set`. Instead, the preference is set when
turning the web client on and off via localapi. In a subsequent commit,
the pref will be used to automatically start the web client on startup
when appropriate.
Updates tailscale/corp#14335
Signed-off-by: Will Norris <will@tailscale.com>
Allows for serving the web interface from tailscaled, with the
ability to start and stop the server via localapi endpoints
(/web/start and /web/stop).
This will be used to run the new full management web client,
which will only be accessible over Tailscale (with an extra auth
check step over noise) from the daemon. This switch also allows
us to run the web interface as a long-lived service in environments
where the CLI version is restricted to CGI, allowing us to manage
certain auth state in memory.
ipn/ipnlocal/web is stubbed out in ipn/ipnlocal/web_stub for
ios builds to satisfy ios restriction from adding "text/template"
and "html/template" dependencies.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
* ipn/localapi: add endpoint to handle APNS payloads
Fixes#9971. This adds a new `handle-push-message` local API endpoint. When an APNS payload is delivered to the main app, this endpoint can be used to forward the JSON body of the message to the backend, making a POST request.
cc @bradfitz
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
* Address comments from code review
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
---------
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
For a serve config with a path handler, ensure the caller is a local administrator on Windows.
updates #8489
Signed-off-by: Tyler Smalley <tyler@tailscale.com>
On Windows, the idiomatic way to check access on a named pipe is for
the server to impersonate the client on its current OS thread, perform
access checks using the client's access token, and then revert the OS
thread's access token back to its true self.
The access token is a better representation of the client's rights than just
a username/userid check, as it represents the client's effective rights
at connection time, which might differ from their normal rights.
This patch updates safesocket to do the aforementioned impersonation,
extract the token handle, and then revert the impersonation. We retain
the token handle for the remaining duration of the connection (the token
continues to be valid even after we have reverted back to self).
Since the token is a property of the connection, I changed ipnauth to wrap
the concrete net.Conn to include the token. I then plumbed that change
through ipnlocal, ipnserver, and localapi as necessary.
I also added a PermitLocalAdmin flag to the localapi Handler which I intend
to use for controlling access to a few new localapi endpoints intended
for configuring auto-update.
Updates https://github.com/tailscale/tailscale/issues/755
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
In addition to the new policy keys for the new options, some
already-in-use but missing policy keys are also being added to
util/syspolicy.
Updates ENG-2133
Change-Id: Iad08ca47f839ea6a65f81b76b4f9ef21183ebdc6
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
clientupdate.Updater will have a non-nil Update func in a few cases
where it doesn't actually perform an update:
* on Arch-like distros, where it prints instructions on how to update
* on macOS app store version, where it opens the app store page
Add a new clientupdate.Arguments field to cause NewUpdater to fail when
we hit one of these cases. This results in c2n updates being "not
supported" and `tailscale set --auto-update` returning an error.
Updates #755
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
It would end up resetting whatever hostinfo we had constructed
and leave the backend statemachine in a broken state.
This fixes that by storing the PushDeviceToken on the LocalBackend
and populating it on Hostinfo before passing it to controlclient.
Updates tailscale/corp#8940
Updates tailscale/corp#15367
Signed-off-by: Maisem Ali <maisem@tailscale.com>
The value being passed was the same as whats on b.hostinfo, so just
use that directly.
Updates #cleanup
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Go has no way to explicitly identify Go struct as effectively a tuple,
so staticcheck assumes any external use of unkeyed literals is wrong.
Updates #cleanup
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This adds a check to prevent changes to ServeConfig if tailscaled
is run with a Locked config.
Missed in 1fc3573446.
Updates #1412
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit makes two changes to the web client auth flow error
handling:
1. Properly passes back the error code from the noise request from
the localapi. Previously we were using io.Copy, which was always
setting a 200 response status code.
2. Clean up web client browser sessions on any /wait endpoint error.
This avoids the user getting in a stuck state if something goes
wrong with their auth path.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Ensure that when a userspace proxy config is reloaded,
connections for any removed proxies are safely closed
Updates tailscale/tailscale#9725
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Initiates http/h2c transport for userspace proxy
backend lazily and at most once.
Updates tailscale/tailscale#9725
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Connects serveTailscaleAuth to the localapi webclient endpoint
and pipes auth URLs and session cookies back to the browser to
redirect users from the frontend.
All behind debug flags for now.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Updates userspace proxy to detect plaintext grpc requests
using the preconfigured host prefix and request's content
type header and ensure that these will be proxied over h2c.
Updates tailscale/tailscale#9725
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
While the previous logic was correct, it did not perform well.
Resuming is a dance between the client and server, where
1. the client requests hashes for a partial file,
2. the server then computes those hashes,
3. the client computes hashes locally and compares them.
4. goto 1 while the partial file still has data
While step 2 is running, the client is sitting idle.
While step 3 is running, the server is sitting idle.
By streaming over the block hash immediately after the server
computes it, the client can start checking the hash,
while the server works on the next hash (in a pipelined manner).
This performs dramatically better and also uses less memory
as we don't need to hold a list of hashes, but only need to
handle one hash at a time.
There are two detriments to this approach:
* The HTTP API relies on a JSON stream,
which is not a standard REST-like pattern.
However, since we implement both client and server,
this is fine.
* While the stream is on-going, we hold an open file handle
on the server side while the file is being hashed.
On really slow streams, this could hold a file open forever.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
Minor fixes:
* The branch for listing or hashing partial files was inverted.
* The host for peerapi call needs to be real (rather than bogus).
* Handle remote peers that don't support resuming.
* Make resume failures non-fatal (since we can still continue).
This was tested locally, end-to-end system test is future work.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
Two bug fixes:
1. when tailscale update is executed as root, `os.UserCacheDir` may
return an error because `$XDG_CACHE_HOME` and `$HOME` are not set;
fallback to `os.TempDir` in those cases
2. on some weird distros (like my EndeavourOS), `/usr/sbin` is just a
symlink to `/usr/bin`; when we resolve `tailscale` binary path from
`tailscaled`, allow `tailscaled` to be in either directory
Updates #755
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
File resumption requires keeping partial files around for some time,
but we must still eventually delete them if never resumed.
Thus, we implement asynchronous file deletion, which could
spawn a background goroutine to delete the files.
We also use the same mechanism for deleting files on Windows,
where a file can't be deleted if there is still an open file handle.
We can enqueue those with the asynchronous file deleter as well.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
The old code would always retain value `true` if it was set once, even
if you then change `prefs.AutoUpdate.Apply` to `false`.
Instead of using the previous value, use the default (envknob) value to
OR with.
Updates #755
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This change:
* adds a partial files peerAPI endpoint to get a list of partial files
* adds a helper function to extract the basename of a file
* updates the peer put peerAPI endpoint
* updates the file put localapi endpoint to allow resume functionality
Updates #14772
Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
The Sparkle-based update is not quite working yet. Make `NewUpdater`
return `ErrUnsupported` for it to avoid the proliferation of exceptions
up the stack.
Updates #755
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Debug endpoint for the web client's auth flow to talk back to the
control server. Restricted behind a feature flag on control.
We will either be removing this debug endpoint, or renaming it
before launching the web client updates.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit changes the PostureChecking syspolicy key to be a
PreferenceOption(user-defined, always, never) instead of Bool.
This aligns better with the defaults implementation on macOS allowing
CLI arguments to be read when user-defined or no defaults is set.
Updates #tailscale/tailscale/5902
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
We add the following API:
* type FileChecksums
* type Checksum
* func Manager.PartialFiles
* func Manager.HashPartialFile
* func ResumeReader
The Manager methods provide the ability to query for partial files
and retrieve a list of checksums for a given partial file.
The ResumeReader function is a helper that wraps an io.Reader
to discard content that is identical locally and remotely.
The FileChecksums type represents the checksums of a file
and is safe to JSON marshal and send over the wire.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
We were eagerly doing a synchronous renewal of the cert while
trying to serve traffic. Instead of that, just do the cert
renewal in the background and continue serving traffic as long
as the cert is still valid.
This regressed in c1ecae13ab when
we introduced ARI support and were trying to make the experience
of `tailscale cert` better. However, that ended up regressing
the experience for tsnet as it would not always doing the renewal
synchronously.
Fixes#9783
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Changes made:
* Move all HTTP related functionality from taildrop to ipnlocal.
* Add two arguments to taildrop.Manager.PutFile to specify
an opaque client ID and a resume offset (both unused for now).
* Cleanup the logic of taildrop.Manager.PutFile
to be easier to follow.
* Implement file conflict handling where duplicate files are renamed
(e.g., "IMG_1234.jpg" -> "IMG_1234 (2).jpg").
* Implement file de-duplication where "renaming" a partial file
simply deletes it if it already exists with the same contents.
* Detect conflicting active puts where a second concurrent put
results in an error.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
These log paths were actually unexpected until the refactor in
fe95d81b43. This moves the logs
to the callsites where they are actually unexpected.
Fixes#9670
Signed-off-by: Maisem Ali <maisem@tailscale.com>
The current structure meant that we were embedding netstack in
the tailscale CLI and in the GUIs. This removes that by isolating
the checksum munging to a different pkg which is only called from
`net/tstun`.
Fixes#9756
Signed-off-by: Maisem Ali <maisem@tailscale.com>
The whois handler was documented as taking IP (e.g. 100.101.102.103)
or IP:port (e.g. usermode 127.0.0.1:1234) but that got broken at some point
and we started requiring a port always. Fix that.
Also, found in the process of adding tests: fix the CapMap lookup in
userspace mode (it was always returning the caps of 127.0.0.1 in
userspace mode). Fix and test that too.
Updates #9714
Change-Id: Ie9a59744286522fa91c4b70ebe89a1e94dbded26
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit adds support for getting serial numbers from SMBIOS
on Windows/Linux (and BSD) using go-smbios.
Updates #5902
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
Changes made:
* Unexport declarations specific to internal taildrop functionality.
* Document all exported functionality.
* Move TestRedactErr to the taildrop package.
* Rename and invert Handler.DirectFileDoFinalRename as AvoidFinalRename.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This is being moved to taildrop, so clean it up to stop depending
on so much unreleated functionality by removing a dependency
on peerAPIHandler.
Updates tailscale/corp#14772
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
Over time all taildrop functionality will be contained in the
taildrop package. This will include end to end unit tests. This is
simply the first smallest piece to move over.
There is no functionality change in this commit.
Updates tailscale/corp#14772
Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
Co-authored-by: Joseph Tsai <joetsai@tailscale.com>
Adds `getTailscaleBrowserSession` to pull the user's session out of
api requests, and `serveTailscaleAuth` to provide the "/api/auth"
endpoint for browser to request auth status and new sessions.
Updates tailscale/corp#14335
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Advertise it on Android (it looks like it already works once advertised).
And both advertise & likely fix it on iOS. Yet untested.
Updates #9672
Change-Id: If3b7e97f011dea61e7e75aff23dcc178b6cf9123
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>