Compare commits

...

904 Commits

Author SHA1 Message Date
Jenny Zhang
78dc8622d7 VERSION.txt: this is v1.64.0
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2024-04-11 17:29:54 +00:00
James Tucker
a2eb1c22b0 wgengine/magicsock: allow disco communication without known endpoints
Just because we don't have known endpoints for a peer does not mean that
the peer should become unreachable. If we know the peers key, it should
be able to call us, then we can talk back via whatever path it called us
on. First step - don't drop the packet in this context.

Updates tailscale/corp#19106

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-11 09:29:49 -07:00
Patrick O'Doherty
7f4cda23ac scripts/installer.sh: add rpm GPG key import (#11686)
Extend the `zypper` install to import importing the GPG key used to sign
the repository packages.

Updates #11635

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-04-10 16:58:35 -07:00
James Tucker
8fa3026614 tsweb: switch to fastuuid for request ID generation
Request ID generation appears prominently in some services cumulative
allocation rate, and while this does not eradicate this issue (the API
still makes UUID objects), it does improve the overhead of this API and
reduce the amount of garbage that it produces.

Updates tailscale/corp#18266
Updates tailscale/corp#19054

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 14:05:20 -07:00
James Tucker
d0f3fa7d7e util/fastuuid: add a more efficient uuid generator
This still generates github.com/google/uuid UUID objects, but does so
using a ChaCha8 CSPRNG from the stdlib rand/v2 package. The public API
is backed by a sync.Pool to provide good performance in highly
concurrent operation.

Under high load the read API produces a lot of extra garbage and
overhead by way of temporaries and syscalls. This implementation reduces
both to minimal levels, and avoids any long held global lock by
utilizing sync.Pool.

Updates tailscale/corp#18266
Updates tailscale/corp#19054

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 14:05:20 -07:00
James Tucker
db760d0bac cmd/tailscaled: move cleanup to an implicit action during startup
This removes a potentially increased boot delay for certain boot
topologies where they block on ExecStartPre that may have socket
activation dependencies on other system services (such as
systemd-resolved and NetworkManager).

Also rename cleanup to clean up in affected/immediately nearby places
per code review commentary.

Fixes #11599

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 12:44:08 -07:00
Nick Khyl
8d83adde07 util/winutil/winenv: add package for current Windows environment details
Package winenv provides information about the current Windows environment.
This includes details such as whether the device is a server or workstation,
and if it is AD domain-joined, MDM-registered, or neither.

Updates tailscale/corp#18342

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-09 13:25:37 -05:00
Paul Scott
da4e92bf01 cmd/tailscale/cli: prefix all --help usages with "tailscale ...", some tidying
Also capitalises the start of all ShortHelp, allows subcommands to be hidden
with a "HIDDEN: " prefix in their ShortHelp, and adds a TS_DUMP_HELP envknob
to look at all --help messages together.

Fixes #11664

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-09 12:52:34 +01:00
Percy Wegmann
9da135dd64 cmd/tailscale/cli: moved share.go to drive.go
Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-08 20:11:20 -05:00
Percy Wegmann
1e0ebc6c6d cmd/tailscale/cli: rename share command to drive
Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-08 20:11:20 -05:00
Joe Tsai
b4ba492701 logtail: require Buffer.Write to not retain the provided slice (#11617)
Buffer.Write has the exact same signature of io.Writer.Write.
The latter requires that implementations to never retain
the provided input buffer, which is an expectation that most
users will have when they see a Write signature.

The current behavior of Buffer.Write where it does retain
the input buffer is a risky precedent to set.
Switch the behavior to match io.Writer.Write.

There are only two implementations of Buffer in existence:
* logtail.memBuffer
* filch.Filch

The former can be fixed by cloning the input to Write.
This will cause an extra allocation in every Write,
but we can fix that will pooling on the caller side
in a follow-up PR.

The latter only passes the input to os.File.Write,
which does respect the io.Writer.Write requirements.

Updates #cleanup
Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-08 15:01:07 -07:00
Irbe Krumina
231e44e742 Revert "cmd/{k8s-nameserver,k8s-operator},k8s-operator: add a kube nameserver, make operator deploy it (#11017)" (#11669)
Temporarily reverting this PR to avoid releasing
half finished featue.

This reverts commit 9e2f58f846.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-08 21:31:52 +01:00
Andrea Gottardo
0001237253 docs/policy: update ADMX and ADML files with new Windows 1.62 syspolicies
Updates ENG-2776

Updates the .admx and .adml files to include the new ManagedByOrganizationName, ManagedByCaption and ManagedByURL system policies, added in Tailscale v1.62 for Windows.

Co-authored-by: Andrea Gottardo <andrea@gottardo.me>
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-08 15:21:27 -05:00
Brad Fitzpatrick
b27238b654 derp/derphttp: don't block in LocalAddr method
The derphttp.Client mutex is held during connects (for up to 10
seconds) so this LocalAddr method (blocking on said mutex) could also
block for up to 10 seconds, causing a pileup upstream in
magicsock/wgengine and ultimately a watchdog timeout resulting in a
crash.

Updates #11519

Change-Id: Idd1d94ee00966be1b901f6899d8b9492f18add0f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 10:57:05 -07:00
Brad Fitzpatrick
e6983baa73 cmd/tailscale/cli: fix macOS crash reading envknob in init (#11667)
And add a test.

Regression from a5e1f7d703

Fixes tailscale/corp#19036

Change-Id: If90984049af0a4820c96e1f77ddf2fce8cb3043f

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 10:22:31 -07:00
Chloé Vulquin
0f3a292ebd cli/configure: respect $KUBECONFIG (#11604)
cmd/tailscale/cli: respect $KUBECONFIG

* `$KUBECONFIG` is a `$PATH`-like: it defines a *list*.
`tailscale config kubeconfig` works like the rest of the
ecosystem so that if $KUBECONFIG is set it will write to the first existant file in the list, if none exist then
the final entry in the list.
* if `$KUBECONFIG` is an empty string, the old logic takes over.

Notes:

* The logic for file detection is inlined based on what `kind` does.
Technically it's a race condition, since the file could be removed/added
in between the processing steps, but the fallout shouldn't be too bad.
https://github.com/kubernetes-sigs/kind/blob/v0.23.0-alpha/pkg/cluster/internal/kubeconfig/internal/kubeconfig/paths.go

* The sandboxed (App Store) variant relies on a specific temporary
entitlement to access the ~/.kube/config file.
The entitlement is only granted to specific files, and so is not
applicable to paths supplied by the user at runtime.
While there may be other ways to achieve this access to arbitrary
kubeconfig files, it's out of scope for now.

Updates #11645

Signed-off-by: Chloé Vulquin <code@toast.bunkerlabs.net>
2024-04-08 16:49:43 +01:00
Brad Fitzpatrick
c71e8db058 cmd/tailscale/cli: stop spamming os.Stdout/os.Stderr in tests
After:

    bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
    bradfitz@book1pro tailscale.com % ./cli.test
    bradfitz@book1pro tailscale.com %

Before:

    bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
    bradfitz@book1pro tailscale.com % ./cli.test

    Warning: funnel=on for foo.test.ts.net:443, but no serve config
             run: `tailscale serve --help` to see how to configure handlers

    Warning: funnel=on for foo.test.ts.net:443, but no serve config
             run: `tailscale serve --help` to see how to configure handlers
    USAGE
      funnel <serve-port> {on|off}
      funnel status [--json]

    Funnel allows you to publish a 'tailscale serve'
    server publicly, open to the entire internet.

    Turning off Funnel only turns off serving to the internet.
    It does not affect serving to your tailnet.

    SUBCOMMANDS
      status  show current serve/funnel status
    error: path must be absolute

    error: invalid TCP source "localhost:5432": missing port in address

    error: invalid TCP source "tcp://somehost:5432"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:5432error: invalid TCP source "tcp://somehost:0"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:0error: invalid TCP source "tcp://somehost:65536"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:65536error: path must be absolute

    error: cannot serve web; already serving TCP

    You don't have permission to enable this feature.

This also moves the color handling up to a generic spot so it's
not just one subcommand doing it itself. See
https://github.com/tailscale/tailscale/issues/11626#issuecomment-2041795129

Fixes #11643
Updates #11626

Change-Id: I3a49e659dcbce491f4a2cb784be20bab53f72303
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 06:46:45 -07:00
Anton Tolchanov
5336362e64 prober: export probe class and metrics from bandwidth prober
- Wrap each prober function into a probe class that allows associating
  metric labels and custom metrics with a given probe;
- Make sure all existing probe classes set a `class` metric label;
- Move bandwidth probe size from being a metric label to a separate
  gauge metric; this will make it possible to use it to calculate
  average used bandwidth using a PromQL query;
- Also export transfer time for the bandwidth prober (more accurate than
  the total probe time, since it excludes connection establishment
  time).

Updates tailscale/corp#17912

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-04-08 12:02:58 +01:00
Anton Tolchanov
21671ca374 prober: remove unused notification code
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-04-08 12:02:58 +01:00
Brad Fitzpatrick
b0fbd85592 net/tsdial: partially fix "tailscale nc" (UserDial) on macOS
At least in the case of dialing a Tailscale IP.

Updates #4529

Change-Id: I9fd667d088a14aec4a56e23aabc2b1ffddafa3fe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-07 16:04:32 -07:00
Brad Fitzpatrick
a5e1f7d703 ipn/{ipnlocal,localapi}: add API to toggle use of exit node
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-07 16:01:00 -07:00
Maisem Ali
3f4c5daa15 wgengine/netstack: remove SubnetRouterWrapper
It was used when we only supported subnet routers on linux
and would nil out the SubnetRoutes slice as no other router
worked with it, but now we support subnet routers on ~all platforms.

The field it was setting to nil is now only used for network logging
and nowhere else, so keep the field but drop the SubnetRouterWrapper
as it's not useful.

Updates #cleanup

Change-Id: Id03f9b6ec33e47ad643e7b66e07911945f25db79
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-04-07 15:44:41 -07:00
alexelisenko
fe22032fb3 net/dns/{publicdns,resolver}: add start of Control D support
Updates #7946

[@bradfitz fixed up version of #8417]

Change-Id: I1dbf6fa8d525b25c0d7ad5c559a7f937c3cd142a
Signed-off-by: alexelisenko <39712468+alexelisenko@users.noreply.github.com>
Signed-off-by: Alex Paguis <alex@windscribe.com>
2024-04-07 11:55:37 -07:00
Brad Fitzpatrick
aa084a29c6 ipn/ipnlocal: name the unlockOnce type, plumb more, add Unlock method
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>
2024-04-06 21:49:23 -07:00
Brad Fitzpatrick
5e7c0b025c ipn/ipnlocal: add some "lockedOnEntry" helpers + guardrails, fix bug
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>
2024-04-06 20:43:54 -07:00
Flakes Updater
efb710d0e5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-04-06 15:12:24 -07:00
Brad Fitzpatrick
38377c37b5 ipn/localapi: sort localapi handler map keys
Updates #cleanup

Change-Id: I750ed8d033954f1f8786fb35dd16895bb1c5af8e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-05 20:44:11 -07:00
Maisem Ali
21b32b467e tsweb: handle panics in retHandler
We would have incomplete stats and missing logs in cases
of panics.

Updates tailscale/corp#18687

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-04-05 18:47:21 -07:00
Brad Fitzpatrick
ac2522092d cmd/tailscale/cli: make exit-node list not random
The output was changing randomly per run, due to range over a map.

Then some misc style tweaks I noticed while debugging.

Fixes #11629

Change-Id: I67aef0e68566994e5744d4828002f6eb70810ee1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-05 18:19:50 -07:00
James Tucker
6e334e64a1 net/netcheck,wgengine/magicsock: align DERP frame receive time heuristics
The netcheck package and the magicksock package coordinate via the
health package, but both sides have time based heuristics through
indirect dependencies. These were misaligned, so the implemented
heuristic aimed at reducing DERP moves while there is active traffic
were non-operational about 3/5ths of the time.

It is problematic to setup a good test for this integration presently,
so instead I added comment breadcrumbs along with the initial fix.

Updates #8603

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-05 13:04:42 -07:00
Irbe Krumina
1fbaf26106 util/linuxfw: fix chain comparison (#11639)
Don't compare pointer fields by pointer value, but by the actual value

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-05 19:43:58 +01:00
Charlotte Brandhorst-Satzkorn
8c75da27fc drive: move normalizeShareName into pkg drive and make func public (#11638)
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>
2024-04-05 11:43:13 -07:00
Will Morrison
306bacc669 cmd/tailscale/cli: Add CLI command to update certs on Synology devices.
Fixes #4674

Signed-off-by: Will Morrison <william.barr.morrison@gmail.com>
2024-04-05 07:08:46 -07:00
Brad Fitzpatrick
9699bb0a20 metrics: fix outdated docs on MultiLabelMap
And make NewMultiLabelMap panic earlier (at construction time)
if the comparable struct type T violates the documented rules,
rather than panicking at Add time.

Updates #cleanup

Change-Id: Ib1a03babdd501b8d699c4f18b1097a56c916c6d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-04 20:53:47 -07:00
Joonas Kuorilehto
fe0cfec4ad wgengine/router: enable ip forwarding on gokrazy
Only on Gokrazy, set sysctls to enable IP forwarding so subnet routing
and advertised exit node works.

Fixes #11405

Signed-off-by: Joonas Kuorilehto <joneskoo@derbian.fi>
2024-04-04 20:48:55 -07:00
Joe Tsai
4bbac72868 util/truncate: support []byte as well (#11614)
There are no mutations to the input,
so we can support both ~string and ~[]byte just fine.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-04 14:38:16 -07:00
Charlotte Brandhorst-Satzkorn
98cf71cd73 tailscale: switch tailfs to drive syntax for api and logs (#11625)
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>
2024-04-04 13:07:58 -07:00
Percy Wegmann
853e3e29a0 wgengine/router: provide explicit hook to signal Android when VPN needs to be reconfigured
This allows clients to avoid establishing their VPN multiple times when
both routes and DNS are changing in rapid succession.

Updates tailscale/corp#18928

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-04 12:56:49 -05:00
Joe Tsai
1a38d2a3b4 util/zstdframe: support specifying a MaxWindowSize (#11595)
Specifying a smaller window size during compression
provides a knob to tweak the tradeoff between memory usage
and the compression ratio.

Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-04 10:46:20 -07:00
Andrew Dunham
7d7d159824 prober: support creating multiple probes in ForEachAddr
So that we can e.g. check TLS on multiple ports for a given IP.

Updates tailscale/corp#16367

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I81d840a4c88138de1cbb2032b917741c009470e6
2024-04-04 13:04:16 -04:00
Andrew Dunham
ac574d875c prober: add helper function to check all IPs for a DNS hostname
This allows us to check all IP addresses (and address families) for a
given DNS hostname while dynamically discovering new IPs and removing
old ones as they're no longer valid.

Also add a testable example that demonstrates how to use it.

Alternative to #11610
Updates tailscale/corp#16367

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6d6f39bafc30e6dfcf6708185d09faee2a374599
2024-04-04 11:11:33 -04:00
Brad Fitzpatrick
8d7894c68e clientupdate, net/dns: fix some "tailsacle" typos
Updates #cleanup

Change-Id: I982175e74b0c8c5b3e01a573e5785e6596b7ac39
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 21:08:25 -07:00
Brad Fitzpatrick
92d3f64e95 go.toolchain.rev: bump to Go 1.22.2
Update tailscale/corp#18893

Change-Id: I4c04f5153ad43429d7f510c9ac2194c3b2fbc6c1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 11:11:07 -07:00
Charlotte Brandhorst-Satzkorn
93618a3518 tailscale: update tailfs functions and vars to use drive naming (#11597)
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>
2024-04-03 10:09:58 -07:00
Brad Fitzpatrick
2409661a0d control/controlclient: delete old naclbox code, require ts2021 Noise
Updates #11585
Updates tailscale/corp#18882

Change-Id: I90e2e4a211c58d429e2b128604614dde18986442
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 09:17:27 -07:00
Brad Fitzpatrick
b9611461e5 ipn/ipnlocal: q-encode (RFC 2047) Tailscale serve header values
Updates #11603

RELNOTE=Tailscale serve headers are now RFC 2047 Q-encoded

Change-Id: I1314b65ecf5d39a5a601676346ec2c334fdef042
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 09:08:29 -07:00
Claire Wang
262fa8a01e ipn/ipnlocal: populate peers' capabilities (#11365)
Populates capabilties field of peers in ipn status.
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-04-03 10:55:28 -04:00
James Tucker
9eaa56df93 tsweb: update doc on BucketedStatsOptions.Finish to match behavior
I originally came to update this to match the documented behavior, but
the code is deliberately avoiding this behavior currently, making it
hard to decide how to update this. For now just align the documentation
to the behavior.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-02 17:22:59 -07:00
Charlotte Brandhorst-Satzkorn
14683371ee tailscale: update tailfs file and package names (#11590)
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>
2024-04-02 13:32:30 -07:00
Brad Fitzpatrick
1c259100b0 cmd/{derper,derpprobe}: add --version flag
Fixes #11582

Change-Id: If99fc1ab6b89d624fbb07bd104dd882d2c7b50b4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-02 12:48:07 -07:00
Patrick O'Doherty
1535d0feca safeweb: move http.Serve for HTTP redirects into lib (#11592)
Refactor the interaction between caller/library when establishing the
HTTP to HTTPS redirects by moving the call to http.Serve into safeweb.
This makes linting for other uses of http.Serve easier without having to
account for false positives created by the old interface.

Updates https://github.com/tailscale/corp/issues/8027

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-04-02 12:04:24 -07:00
James Tucker
f384742375 net/packet: allow more ICMP errors
We now allow some more ICMP errors to flow, specifically:

- ICMP parameter problem in both IPv4 and IPv6 (corrupt headers)
- ICMP Packet Too Big (for IPv6 PMTU)

Updates #311
Updates #8102
Updates #11002

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-02 11:31:49 -07:00
Irbe Krumina
92ca770b8d util/linuxfw: fix MSS clamping in nftables mode (#11588)
MSS clamping for nftables was mostly not ran due to to an earlier rule in the FORWARD chain issuing accept verdict.
This commit places the clamping rule into a chain of its own to ensure that it gets ran.

Updates tailscale/tailscale#11002

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-02 19:31:33 +01:00
Kyle Carberry
27038ee3c2 hostinfo: cache device model to speed up init
This was causing a relatively consistent ~10ms of delay on Linux.

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2024-04-02 09:09:43 -07:00
Brad Fitzpatrick
ec87e219ae logtail: delete unused code from old way to configure zstd
Updates #cleanup

Change-Id: I666ecf08ea67e461adf2a3f4daa9d1753b2dc1e4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-01 20:43:06 -07:00
Joe Tsai
e2586bc674 logtail: always zstd compress with FastestCompression and LowMemory (#11583)
This is based on empirical testing using actual logs data.

FastestCompression only incurs a marginal <1% compression ratio hit
for a 2.25x reduction in memory use for small payloads
(which are common if log uploads happen at a decently high frequency).
The memory savings for large payloads is much lower
(less than 1.1x reduction).

LowMemory only incurs a marginal <5% hit on performance
for a 1.6-2.0x reduction in memory use for small or large payloads.

The memory gains for both settings justifies the loss of benefits,
which are arguably minimal.

tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-01 18:12:09 -07:00
James Tucker
7558a1d594 ipn/ipnlocal: disable sockstats on (unstable) mobile by default
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>
2024-04-01 14:44:20 -07:00
Asutorufa
e20ce7bf0c net/dns: close ctx when close dns directManager
Signed-off-by: Asutorufa <16442314+Asutorufa@users.noreply.github.com>
2024-03-29 20:47:03 -07:00
Will Norris
1d2af801fa .github/workflows: remove go-licenses action
This is now handled by an action running in corp.

Updates tailscale/corp#18803

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-29 19:38:13 -07:00
License Updater
e80b99cdd1 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-29 16:32:25 -07:00
Andrew Lytvynov
5aa4cfad06 safeweb: detect mux handler conflicts (#11562)
When both muxes match, and one of them is a wildcard "/" pattern (which
is common in browser muxes), choose the more specific pattern.
If both are non-wildcard matches, there is a pattern overlap, so return
an error.

Updates https://github.com/tailscale/corp/issues/8027

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-29 16:07:09 -06:00
Brad Fitzpatrick
e7599c1f7e logtail: prevent js/wasm clients from picking TLS client cert
Corp details:
https://github.com/tailscale/corp/issues/18177#issuecomment-2026598715
https://github.com/tailscale/corp/pull/18775#issuecomment-2027505036

Updates tailscale/corp#18177

Change-Id: I7c03a4884540b8519e0996088d085af77991f477
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-29 13:18:33 -07:00
Irbe Krumina
5fb721d4ad util/linuxfw,wgengine/router: skip IPv6 firewall configuration in partial iptables mode (#11546)
We have hosts that support IPv6, but not IPv6 firewall configuration
in iptables mode.
We also have hosts that have some support for IPv6 firewall
configuration in iptables mode, but do not have iptables filter table.
We should:
- configure ip rules for all hosts that support IPv6
- only configure firewall rules in iptables mode if the host
has iptables filter table.

Updates tailscale/tailscale#11540

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-29 05:23:03 +00:00
Patrick O'Doherty
af61179c2f safeweb: add opt-in inline style CSP toggle (#11551)
Allow the use of inline styles with safeweb via an opt-in configuration
item. This will append `style-src "self" "unsafe-inline"` to the default
CSP. The `style-src` directive will be used in lieu of the fallback
`default-src "self"` directive.

Updates tailscale/corp#8027

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-28 13:15:01 -07:00
Brad Fitzpatrick
b0941b79d6 tsweb: make BucketedStats not track 400s, 404s, etc
Updates tailscale/corp#18687

Change-Id: I142ccb1301ec4201c70350799ff03222bce96668
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-28 08:56:33 -07:00
Brad Fitzpatrick
354cac74a9 tsweb/varz: add charset=utf-8 to varz handler
Some of our labels contain UTF-8 and get mojibaked in the browser
right now.

Updates tailscale/corp#18687

Change-Id: I6069cffd6cc8813df415f06bb308bc2fc3ab65c4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-27 19:56:22 -07:00
James Tucker
9401b09028 control/controlclient: move client watchdog to cover initial request
The initial control client request can get stuck in the event that a
connection is established but then lost part way through, without any
ICMP or RST. Ensure that the control client will be restarted by timing
out that initial request as well.

Fixes #11542

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-27 16:02:52 -07:00
Irbe Krumina
9b5176c4d9 cmd/k8s-operator: fix failing tests (#11541)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-27 20:56:07 +00:00
Irbe Krumina
9e2f58f846 cmd/{k8s-nameserver,k8s-operator},k8s-operator: add a kube nameserver, make operator deploy it (#11017)
* cmd/k8s-nameserver,k8s-operator: add a nameserver that can resolve ts.net DNS names in cluster.

Adds a simple nameserver that can respond to A record queries for ts.net DNS names.
It can respond to queries from in-memory records, populated from a ConfigMap
mounted at /config. It dynamically updates its records as the ConfigMap
contents changes.
It will respond with NXDOMAIN to queries for any other record types
(AAAA to be implemented in the future).
It can respond to queries over UDP or TCP. It runs a miekg/dns
DNS server with a single registered handler for ts.net domain names.
Queries for other domain names will be refused.

The intended use of this is:
1) to allow non-tailnet cluster workloads to talk to HTTPS tailnet
services exposed via Tailscale operator egress over HTTPS
2) to allow non-tailnet cluster workloads to talk to workloads in
the same cluster that have been exposed to tailnet over their
MagicDNS names but on their cluster IPs.

Updates tailscale/tailscale#10499

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/k8s-operator/deploy/crds,k8s-operator: add DNSConfig CustomResource Definition

DNSConfig CRD can be used to configure
the operator to deploy kube nameserver (./cmd/k8s-nameserver) to cluster.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/k8s-operator,k8s-operator: optionally reconcile nameserver resources

Adds a new reconciler that reconciles DNSConfig resources.
If a DNSConfig is deployed to cluster,
the reconciler creates kube nameserver resources.
This reconciler is only responsible for creating
nameserver resources and not for populating nameserver's records.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/{k8s-operator,k8s-nameserver}: generate DNSConfig CRD for charts, append to static manifests

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

---------

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-27 20:18:17 +00:00
Patrick O'Doherty
b60c4664c7 safeweb: return http.Handler from safeweb.RedirectHTTP (#11538)
Updates #cleanup

Change the return type of the safeweb.RedirectHTTP method to a handler
that can be passed directly to http.Serve without any http.HandlerFunc
wrapping necessary.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-27 11:44:17 -07:00
Brad Fitzpatrick
3e6306a782 derp/derphttp: make CONNECT Host match request-target's authority-form
This CONNECT client doesn't match what Go's net/http.Transport does
(making the two values match).  This makes it match.

This is all pretty unspecified but most clients & doc examples show
these matching. And some proxy implementations (such as Zscaler) care.

Updates tailscale/corp#18716

Change-Id: I135c5facbbcec9276faa772facbde1bb0feb2d26
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-27 11:36:28 -07:00
Patrick O'Doherty
8f27520633 safeweb: init (#11467)
Updates https://github.com/tailscale/corp/issues/8027

Safeweb is a wrapper around http.Server & tsnet that encodes some
application security defaults.

Safeweb asks developers to split their HTTP routes into two
http.ServeMuxs for serving browser and API-facing endpoints
repsectively. It then wraps these HTTP routes with the
context-appropriate security controls.

safeweb.Server#Serve will serve the HTTP muxes over the provided
listener. Caller are responsible for creating and tearing down their
application's listeners. Applications being served over HTTPS that wish
to implement HTTP redirects can use the Server#HTTPRedirect handler to
do so.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-27 10:10:59 -07:00
Andrea Gottardo
008676f76e cmd/serve: update warning for sandboxed macOS builds (#11530) 2024-03-27 09:03:52 -07:00
Percy Wegmann
66e4d843c1 ipn/localapi: add support for multipart POST to file-put
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>
2024-03-27 08:53:52 -05:00
Percy Wegmann
bed818a978 ipn/localapi: add support for multipart POST to file-put
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>
2024-03-27 08:53:52 -05:00
Maisem Ali
0d8cd1645a go.mod: bump github.com/gaissmai/bart
To pick up https://github.com/gaissmai/bart/pull/17.

Updates #deps

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-03-26 16:06:52 -07:00
Percy Wegmann
eb42a16da9 ipn/ipnlocal: report Taildrive access message on failed responses
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>
2024-03-26 16:37:08 -05:00
Andrew Lytvynov
5d41259a63 cmd/tailscale/cli: remove Beta tag from tailscale update (#11529)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-26 15:28:34 -06:00
Charlotte Brandhorst-Satzkorn
acb611f034 ipn/localipn: introduce logs for tailfs (#11496)
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>
2024-03-26 13:14:43 -07:00
Irbe Krumina
4cbef20569 cmd/k8s-operator: redact auth key from debug logs (#11523)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-26 16:20:32 +00:00
Brad Fitzpatrick
55baf9474f metrics, tsweb/varz: add multi-label map metrics
Updates tailscale/corp#18640

Change-Id: Ia9ae25956038e9d3266ea165537ac6f02485b74c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-26 09:01:04 -07:00
Flakes Updater
90a4d6ce69 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-03-26 08:37:52 -07:00
Brad Fitzpatrick
6d90966c1f logtail: move a scratch buffer to Logger
Rather than pass around a scratch buffer, put it on the Logger.

This is a baby step towards removing the background uploading
goroutine and starting it as needed.

Updates tailscale/corp#18514 (insofar as it led me to look at this code)

Change-Id: I6fd94581c28bde40fdb9fca788eb9590bcedae1b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-25 17:33:42 -07:00
Irbe Krumina
06e22a96b1 .github/workflows: fix path filter for 'Kubernetes manifests' test job (#11520)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-25 19:44:30 +00:00
Chris Milson-Tokunaga
b6dfd7443a Change type of installCRDs (#11478)
Including the double quotes (`"`) around the value made it appear like the helm chart should expect a string value for `installCRDs`.

Signed-off-by: Chris Milson-Tokunaga <chris.w.milson@gmail.com>
2024-03-25 19:11:55 +00:00
Percy Wegmann
8b8b315258 net/tstun: use gaissmai/bart instead of tempfork/device
This implementation uses less memory than tempfork/device,
which helps avoid OOM conditions in the iOS VPN extension when
switching to a Tailnet with ExitNode routing enabled.

Updates tailscale/corp#18514

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-25 12:31:14 -05:00
Andrew Lytvynov
1e7050e73a go.mod: bump github.com/docker/docker (#11515)
There's a vulnerability https://pkg.go.dev/vuln/GO-2024-2659 that
govulncheck flags, even though it's only reachable from tests and
cmd/sync-containers and cannot be exploited there.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-25 10:45:35 -06:00
Brad Fitzpatrick
a36cfb4d3d tailcfg, ipn/ipnlocal, wgengine/magicsock: add only-tcp-443 node attr
Updates tailscale/corp#17879

Change-Id: I0dc305d147b76c409cf729b599a94fa723aef0e0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-25 08:48:25 -07:00
Brad Fitzpatrick
7b34154df2 all: deprecate Node.Capabilities (more), remove PeerChange.Capabilities [capver 89]
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>
2024-03-24 21:08:46 -07:00
Brad Fitzpatrick
4992aca6ec tsweb/varz: flesh out munging of expvar keys into valid Prometheus metrics
From a problem we hit with how badger registers expvars; it broke
trunkd's exported metrics.

Updates tailscale/corp#1297

Change-Id: I42e1552e25f734c6f521b6e993d57a82849464b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-24 20:06:06 -07:00
Brad Fitzpatrick
b104688e04 ipn/ipnlocal, types/netmap: replace hasCapability with set lookup on NetworkMap
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>
2024-03-22 15:30:46 -07:00
Percy Wegmann
8c88853db6 ipn/ipnlocal: add c2n /debug/pprof/allocs endpoint
This behaves the same as typical debug/pprof/allocs.

Updates tailscale/corp#18514

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-22 17:29:59 -05:00
Brad Fitzpatrick
f45594d2c9 control/controlclient: free memory on iOS before full netmap work
Updates tailscale/corp#18514

Change-Id: I8d0330334b030ed8692b25549a0ee887ac6d7188
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-22 09:02:19 -07:00
James Tucker
e0f97738ee localapi: reduce garbage production in bus watcher
Updates #optimization

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-21 17:50:20 -07:00
James Tucker
3f7313dbdb util/linuxfw,wgengine/router: enable IPv6 configuration when netfilter is disabled
Updates #11434

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-21 16:10:47 -07:00
Brad Fitzpatrick
8444937c89 control/controlclient: fix panic regression from earlier load balancer hint header
In the recent 20e9f3369 we made HealthChangeRequest machine requests
include a NodeKey, as it was the oddball machine request that didn't
include one. Unfortunately, that code was sometimes being called (at
least in some of our integration tests) without a node key due to its
registration with health.RegisterWatcher(direct.ReportHealthChange).

Fortunately tests in corp caught this before we cut a release. It's
possible this only affects this particular integration test's
environment, but still worth fixing.

Updates tailscale/corp#1297

Change-Id: I84046779955105763dc1be5121c69fec3c138672
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 12:54:58 -07:00
Joe Tsai
85febda86d all: use zstdframe where sensible (#11491)
Use the zstdframe package where sensible instead of plumbing
around our own zstd.Encoder just for stateless operations.

This causes logtail to have a dependency on zstd,
but that's arguably okay since zstd support is implicit
to the protocol between a client and the logging service.
Also, virtually every caller to logger.NewLogger was
manually setting up a zstd.Encoder anyways,
meaning that zstd was functionally always a dependency.

Updates #cleanup
Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-03-21 12:20:38 -07:00
Joe Tsai
d4bfe34ba7 util/zstdframe: add package for stateless zstd compression (#11481)
The Go zstd package is not friendly for stateless zstd compression.
Passing around multiple zstd.Encoder just for stateless compression
is a waste of memory since the memory is never freed and seldom
used if no compression operations are happening.

For performance, we pool the relevant Encoder/Decoder
with the specific options set.

Functionally, this package is a wrapper over the Go zstd package
with a more ergonomic API for stateless operations.

This package can be used to cleanup various pre-existing zstd.Encoder
pools or one-off handlers spread throughout our codebases.

Performance:

	BenchmarkEncode/Best               1690        610926 ns/op      25.78 MB/s           1 B/op          0 allocs/op
	    zstd_test.go:137: memory: 50.336 MiB
	    zstd_test.go:138: ratio:  3.269x
	BenchmarkEncode/Better            10000        100939 ns/op     156.04 MB/s           0 B/op          0 allocs/op
	    zstd_test.go:137: memory: 20.399 MiB
	    zstd_test.go:138: ratio:  3.131x
	BenchmarkEncode/Default            15775         74976 ns/op     210.08 MB/s         105 B/op          0 allocs/op
	    zstd_test.go:137: memory: 1.586 MiB
	    zstd_test.go:138: ratio:  3.064x
	BenchmarkEncode/Fastest            23222         53977 ns/op     291.81 MB/s          26 B/op          0 allocs/op
	    zstd_test.go:137: memory: 599.458 KiB
	    zstd_test.go:138: ratio:  2.898x
	BenchmarkEncode/FastestLowMemory                   23361         50789 ns/op     310.13 MB/s          15 B/op          0 allocs/op
	    zstd_test.go:137: memory: 334.458 KiB
	    zstd_test.go:138: ratio:  2.898x
	BenchmarkEncode/FastestNoChecksum                  23086         50253 ns/op     313.44 MB/s          26 B/op          0 allocs/op
	    zstd_test.go:137: memory: 599.458 KiB
	    zstd_test.go:138: ratio:  2.900x

	BenchmarkDecode/Checksum                           70794         17082 ns/op     300.96 MB/s           4 B/op          0 allocs/op
	    zstd_test.go:163: memory: 316.438 KiB
	BenchmarkDecode/NoChecksum                         74935         15990 ns/op     321.51 MB/s           4 B/op          0 allocs/op
	    zstd_test.go:163: memory: 316.438 KiB
	BenchmarkDecode/LowMemory                          71043         16739 ns/op     307.13 MB/s           0 B/op          0 allocs/op
	    zstd_test.go:163: memory: 79.347 KiB

We can see that the options are taking effect where compression ratio improves
with higher levels and compression speed diminishes.
We can also see that LowMemory takes effect where the pooled coder object
references less memory than other cases.
We can see that the pooling is taking effect as there are 0 amortized allocations.

Additional performance:

	BenchmarkEncodeParallel/zstd-24                     1857        619264 ns/op        1796 B/op         49 allocs/op
	BenchmarkEncodeParallel/zstdframe-24                1954        532023 ns/op        4293 B/op         49 allocs/op
	BenchmarkDecodeParallel/zstd-24                     5288        197281 ns/op        2516 B/op         49 allocs/op
	BenchmarkDecodeParallel/zstdframe-24                6441        196254 ns/op        2513 B/op         49 allocs/op

In concurrent usage, handling the pooling in this package
has a marginal benefit over the zstd package,
which relies on a Go channel as the pooling mechanism.
In particular, coders can be freed by the GC when not in use.
Coders can be shared throughout the program if they use this package
instead of multiple independent pools doing the same thing.
The allocations are unrelated to pooling as they're caused by the spawning of goroutines.

Updates #cleanup
Updates tailscale/corp#18514
Updates tailscale/corp#17653
Updates tailscale/corp#18005

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-03-21 11:39:20 -07:00
Brad Fitzpatrick
6a860cfb35 ipn/ipnlocal: add c2n pprof option to force a GC
Like net/http/pprof has.

Updates tailscale/corp#18514

Change-Id: I264adb6dcf5732d19707783b29b7273b4ca69cf4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 10:24:06 -07:00
Brad Fitzpatrick
5d1c72f76b wgengine/magicsock: don't use endpoint debug ringbuffer on mobile.
Save some memory.

Updates tailscale/corp#18514

Change-Id: Ibcaf3c6d8e5cc275c81f04141d0f176e2249509b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 06:58:55 -07:00
Andrew Dunham
512fc0b502 util/reload: add new package to handle periodic value loading
This can be used to reload a value periodically, whether from disk or
another source, while handling jitter and graceful shutdown.

Updates tailscale/corp#1297

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iee2b4385c9abae59805f642a7308837877cb5b3f
2024-03-20 18:23:54 -04:00
Adrian Dewhurst
2f7e7be2ea control/controlclient: do not alias peer CapMap
Updates #cleanup

Change-Id: I10fd5e04310cdd7894a3caa3045b86eb0a06b6a0
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-03-20 15:42:44 -04:00
Percy Wegmann
067ed0bf6f ipnlocal: ensure TailFS share notifications are non-nil
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>
2024-03-20 13:54:33 -05:00
Brad Fitzpatrick
20e9f3369d control/controlclient: send load balancing hint HTTP request header
Updates tailscale/corp#1297

Change-Id: I0b102081e81dfc1261f4b05521ab248a2e4a1298
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-20 09:39:41 -07:00
Percy Wegmann
15c58cb77c tailfs: include whitespace in test share and filenames
Since TailFS allows spaces in folder and file names, test with spaces.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-20 09:41:18 -05:00
James Tucker
e37eded256 tool/gocross: add android autoflags (#11465)
Updates tailscale/corp#18202

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-19 16:08:20 -07:00
Claire Wang
221de01745 control/controlclient: fix sending peer capmap changes (#11457)
Instead of just checking if a peer capmap is nil, compare the previous
state peer capmap with the new peer capmap.
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-19 18:56:06 -04:00
Andrew Dunham
6da1dc84de wgengine: fix logger data race in tests
Observed in:
    https://github.com/tailscale/tailscale/actions/runs/8350904950/job/22858266932?pr=11463

Updates #11226

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9b57db4b34b6ad91d240cd9fa7e344fc0376d52d
2024-03-19 18:51:25 -04:00
Andrew Dunham
e382e4cee6 syncs: add Swap method
To mimic sync.Map.Swap, sync/atomic.Value.Swap, etc.

Updates tailscale/corp#1297

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If7627da1bce8b552873b21d7e5ebb98904e9a650
2024-03-19 18:39:44 -04:00
Andrea Gottardo
6288c9b41e version/prop: remove IsMacAppSandboxEnabled (#11461)
Fixes tailscale/corp#18441

For a few days, IsMacAppStore() has been returning `false` on App Store builds (IPN-macOS target in Xcode).

I regressed this in #11369 by introducing logic to detect the sandbox by checking for the APP_SANDBOX_CONTAINER_ID environment variable. I thought that was a more robust approach instead of checking the name of the executable. However, it appears that on recent macOS versions this environment variable is no longer getting set, so we should go back to the previous logic that checks for the executable path, or HOME containing references to macsys.

This PR also adds additional checks to the logic by also checking XPC_SERVICE_NAME in addition to HOME where possible. That environment variable is set inside the network extension, either macos or macsys and is good to look at if for any reason HOME is not set.
2024-03-19 14:50:34 -07:00
Mario Minardi
68d9e49a5b api.md: add missing backtick to GET searchpaths doc (#11459)
Add missing backtick to GET searchpaths api documentation.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-19 11:31:03 -06:00
Will Norris
349799a1ba api.md: format API docs with prettier
Mostly inconsequential minor fixes for consistency.  A couple of changes
to actual JSON examples, but all still very readable, so I think it's
fine.

Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-19 09:23:50 -07:00
Irbe Krumina
b0c3e6f6c5 cmd/k8s-operator,ipn/conf.go: fix --accept-routes for proxies (#11453)
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>
2024-03-19 14:54:17 +00:00
James Tucker
7fe4cbbaf3 types/views: optimize slices contains under some conditions (#11449)
In control there are conditions where the leaf functions are not being
optimized away (i.e. At is not inlined), resulting in undesirable time
spent copying during SliceContains. This optimization is likely
irrelevant to simpler code or smaller structures.

Updates #optimization

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-18 16:19:16 -07:00
Mario Minardi
d2ccfa4edd cmd/tailscale,ipn/ipnlocal: enable web client over quad 100 by default (#11419)
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>
2024-03-18 15:47:21 -06:00
Will Norris
4d747c1833 api.md: document device expiration endpoint
This was originally built for testing node expiration flows, but is also
useful for customers to force device re-auth without actually deleting
the device from the tailnet.

Updates tailscale/corp#18408

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-18 12:49:55 -07:00
Mario Minardi
e0886ad167 ipn/ipnlocal, tailcfg: add disable-web-client node attribute (#11418)
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>
2024-03-18 10:32:33 -06:00
Marwan Sulaiman
da7c3d1753 envknob: ensure f is not nil before using it
This PR fixes a panic that I saw in the mac app where
parsing the env file fails but we don't get to see the
error due to the panic of using f.Name()

Fixes #11425

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2024-03-15 12:46:41 -04:00
Andrea Gottardo
08ebac9acb version,cli,safesocket: detect non-sandboxed macOS GUI (#11369)
Updates ENG-2848

We can safely disable the App Sandbox for our macsys GUI, allowing us to use `tailscale ssh` and do a few other things that we've wanted to do for a while. This PR:

- allows Tailscale SSH to be used from the macsys GUI binary when called from a CLI
- tweaks the detection of client variants in prop.go, with new functions `IsMacSys()`, `IsMacSysApp()` and `IsMacAppSandboxEnabled()`

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-03-14 14:28:06 -07:00
Irbe Krumina
ea55f96310 cmd/tailscale/cli: fix configuring partially empty kubeconfig (#11417)
When a user deletes the last cluster/user/context from their
kubeconfig via 'kubectl delete-[cluster|user|context] command,
kubectx sets the relevant field in kubeconfig to 'null'.
This was breaking our conversion logic that was assuming that the field
is either non-existant or is an array.

Updates tailscale/corp#18320

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-14 20:26:20 +00:00
Anton Tolchanov
cf8948da5f net/routetable: increase route limit used by the test
I was running all tests while preparing a recent stable release, and
this was failing because my computer is connected to a fairly large
tailnet.

```
--- FAIL: TestGetRouteTable (0.01s)
    routetable_linux_test.go:32: expected at least one default route;
    ...
```

```
$ ip route show table 52  | wc -l
1051
```

Updates #cleanup

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-14 16:10:40 +00:00
Andrew Lytvynov
decd9893e4 ipn/ipnlocal: validate domain of PopBrowserURL on default control URL (#11394)
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>
2024-03-13 18:31:07 -06:00
Andrew Lytvynov
48eef9e6eb clientupdate: do not allow msiexec to reboot the OS (#11409)
According to
https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options#promptrestart,
`/promptrestart` is ignored with `/quiet` is set, so msiexec.exe can
sometimes silently trigger a reboot. The best we can do to reduce
unexpected disruption is to just prevent restarts, until the user
chooses to do it. Restarts aren't normally needed for Tailscale updates,
but there seem to be some situations where it's triggered.

Updates #18254

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-13 16:55:24 -06:00
Anton Tolchanov
da3cf12194 VERSION.txt: this is v1.63.0
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 14:51:52 +00:00
Anton Tolchanov
f12d2557f9 prober: add a DERP bandwidth probe
Updates tailscale/corp#17912

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Anton Tolchanov
5018683d58 prober: remove unused derp prober latency measurements
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Anton Tolchanov
205a10b51a prober: export probe counters and cumulative latency
Updates #cleanup

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Andrew Dunham
7429e8912a wgengine/netstack: fix bug with duplicate SYN packets in client limit
This fixes a bug that was introduced in #11258 where the handling of the
per-client limit didn't properly account for the fact that the gVisor
TCP forwarder will return 'true' to indicate that it's handled a
duplicate SYN packet, but not launch the handler goroutine.

In such a case, we neither decremented our per-client limit in the
wrapper function, nor did we do so in the handler function, leading to
our per-client limit table slowly filling up without bound.

Fix this by doing the same duplicate-tracking logic that the TCP
forwarder does so we can detect such cases and appropriately decrement
our in-flight counter.

Updates tailscale/corp#12184

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib6011a71d382a10d68c0802593f34b8153d06892
2024-03-11 08:05:00 -04:00
Brad Fitzpatrick
ad33e47270 ipn/{ipnlocal,localapi}: add debug verb to force spam IPN bus NetMap
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>
2024-03-09 17:47:15 -08:00
Flakes Updater
04fceae898 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-03-09 11:51:43 -08:00
James Tucker
055117ad45 util/linuxfw: fix support for containers without IPv6 iptables filters (#11381)
There are container environments such as GitHub codespaces that have
partial IPv6 support - routing support is enabled at the kernel level,
but lacking IPv6 filter support in the iptables module.

In the specific example of the codespaces environment, this also has
pre-existing legacy iptables rules in the IPv4 tables, as such the
nascent firewall mode detection will always pick iptables.

We would previously fault trying to install rules to the filter table,
this catches that condition earlier, and disables IPv6 support under
these conditions.

Updates #5621
Updates #11344
Updates #11354

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:46:21 -08:00
James Tucker
43fba6e04d util/linuxfw: correct logical error in NAT table check (#11380)
Updates #11344
Updates #11354

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:35:13 -08:00
panchajanya
50a570a83f Code Improvements (#11311)
build_docker, update-flake: cleanup and apply shellcheck fixes

Was editing this file to match my needs while shellcheck warnings
bugged me out.
REV isn't getting used anywhere. Better remove it.

Updates #cleanup

Signed-off-by: Panchajanya1999 <kernel@panchajanya.dev>
Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:24:36 -08:00
Percy Wegmann
e496451928 ipn,cmd/tailscale,client/tailscale: add support for renaming TailFS shares
- 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>
2024-03-08 14:48:26 -06:00
Percy Wegmann
6c160e6321 ipn,tailfs: tie TailFS share configuration to user profile
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>
2024-03-08 14:48:26 -06:00
Percy Wegmann
16ae0f65c0 cmd/viewer: import views when generating byteSliceField
Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-08 14:48:26 -06:00
Andrew Dunham
f072d017bd wgengine/magicsock: don't change DERP home when not connected to control
This pretty much always results in an outage because peers won't
discover our new home region and thus won't be able to establish
connectivity.

Updates tailscale/corp#18095

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic0d09133f198b528dd40c6383b16d7663d9d37a7
2024-03-08 14:15:13 -05:00
Sonia Appasamy
54e52532eb version/mkversion: enforce synology versions within int32 range
Synology requires version numbers are within int32 range. This
change updates the version logic to keep things closer within the
range, and errors on building when the range is exceeded.

Updates #cleanup

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-03-08 12:47:59 -05:00
Claire Wang
74e33b9c50 tailcfg: bump CapabilityVersion (#11368)
bump version for adding NodeAttrSuggestExitNode
remove extra s from NodeAttrSuggestExitNode
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-07 14:17:40 -05:00
Mario Minardi
c662bd9fe7 client/web: dedupe packages in yarn.lock (#11327)
Run yarn-deduplicate on yarn.lock to dedupe packages. This is being done
to reduce the number of redundant packages fetched by yarn when existing
versions in the lockfile satisfy the version dependency we need.

See https://github.com/scinos/yarn-deduplicate for details on the tool
used to perform this deduplication.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-07 09:29:20 -07:00
Andrew Dunham
34176432d6 cmd/derper, types/logger: move log filter to shared package
So we can use it in trunkd to quiet down the logs there.

Updates #5563

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie3177dc33f5ad103db832aab5a3e0e4f128f973f
2024-03-07 11:05:03 -05:00
Irbe Krumina
3047b6274c docs/k8s: don't run subnet router in userspace mode (#11363)
There should not be a need to do that unless we run on host network

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-07 13:56:11 +00:00
Andrew Dunham
9884d06b80 net/interfaces: fix test hang on Darwin
This test could hang because the subprocess was blocked on writing to
the stdout pipe if we find the address we're looking for early in the
output.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I68d82c22a5d782098187ae6d8577e43063b72573
2024-03-06 22:37:40 -05:00
Andrew Dunham
62cf83eb92 go.mod: bump gvisor
The `stack.PacketBufferPtr` type no longer exists; replace it with
`*stack.PacketBuffer` instead.

Updates #8043

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib56ceff09166a042aa3d9b80f50b2aa2d34b3683
2024-03-06 20:22:20 -05:00
Andrew Dunham
8f27d519bb tsweb: add String method to tsweb.RequestID
In case we want to change the format to something opaque later.

Updates tailscale/corp#2549

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie2eac8b885b694be607e9d5101d24b650026d89c
2024-03-06 19:48:04 -05:00
Irbe Krumina
90c4067010 util/linuxfw: add container-friendly IPv6 NAT check (#11353)
Remove IPv6 NAT check when routing is being set up
using nftables.
This is unnecessary as support for nftables was
added after support for IPv6.
https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html
https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources

Additionally, run an extra check for IPv6 NAT support
when the routing is set up with iptables.
This is because the earlier checks rely on
being able to use modprobe and on /proc/net/ip6_tables_names
being populated on start - these conditions are usually not
true in container environments.

Updates tailscale/tailscale#11344

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-06 21:53:51 +00:00
Percy Wegmann
fd942b5384 ipn/ipnlocal: reduce allocations in TailFS share notifications
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>
2024-03-06 14:21:53 -06:00
Percy Wegmann
6f66f5a75a ipn: add comment about thread-safety to StateStore
Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-06 12:42:18 -06:00
Andrea Gottardo
0cb86468ca ipn/localapi: add set-gui-visible endpoint
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>
2024-03-06 10:39:52 -08:00
Percy Wegmann
00373f07ac ipn/ipnlocal: exclude mullvad exit nodes from TailFS peers list
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>
2024-03-06 12:27:32 -06:00
Sonia Appasamy
c58c59ee54 {ipn,cmd/tailscale/cli}: move ServeConfig mutation logic to ipn/serve
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>
2024-03-06 13:26:03 -05:00
Kristoffer Dalby
65255b060b client/tailscale: add postures to UserRuleMatch
Updates tailscale/corp#17770

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-03-06 15:36:17 +01:00
License Updater
d59878e457 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:55:26 -08:00
License Updater
797d75c50a licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:53:52 -08:00
License Updater
6a4e5329c3 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:53:05 -08:00
Andrew Dunham
4338db28f7 wgengine/magicsock: prefer link-local addresses to private ones
Since link-local addresses are definitionally more likely to be a direct
(lower-latency, more reliable) connection than a non-link-local private
address, give those a bit of a boost when selecting endpoints.

Updates #8097

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I93fdeb07de55ba39ba5fcee0834b579ca05c2a4e
2024-03-05 20:32:45 -05:00
Sonia Appasamy
65c3c690cf {ipn/serve,cmd/tailscale/cli}: move some shared funcs to ipn
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>
2024-03-05 14:30:38 -05:00
Brad Fitzpatrick
8780e33500 go.toolchain.rev: bump Go toolchain to 1.22.1
Updates tailscale/corp#18000

Change-Id: I45de95e974ea55b0dac2218b3c82d124c4793390
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-05 10:51:13 -08:00
Paul Scott
2fa20e3787 util/cmpver: add Less/LessEq helper funcs
Updates tailscale/corp#17199

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-03-05 16:57:04 +00:00
Claire Wang
d610f8eec0 tailcfg: add suggest exit node related node attribute (#11329)
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-05 10:54:41 -05:00
Chris Palmer
13853e7f29 tsweb: add more test cases for TestCleanRedirectURL (#11331)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-03-04 17:13:36 -08:00
Irbe Krumina
dff6f3377f docs/k8s: update docs (#11307)
Update docs for static Tailscale deployments on kube
to always use firewall mode autodection when in non-userspace.
Also add a note about running multiple replicas and a few suggestions how folks could do that.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <1687799+knyar@users.noreply.github.com>
2024-03-04 14:59:51 +00:00
Percy Wegmann
232a2d627c tailfs: only impersonate unprivileged user if able to sudo -u as that user
When serving TailFS shares, tailscaled executes another tailscaled to act as a
file server. It attempts to execute this child process as an unprivileged user
using sudo -u. This is important to avoid accessing files as root, which would
result in potential privilege escalation.

Previously, tailscaled assumed that it was running as someone who can sudo -u,
and would fail if it was unable to sudo -u.

With this commit, if tailscaled is unable to sudo -u as the requested user, and
tailscaled is not running as root, then tailscaled executes the the file server
process under the same identity that ran tailscaled, since this is already an
unprivileged identity.

In the unlikely event that tailscaled is running as root but is unable to
sudo -u, it will refuse to run the child file server process in order to avoid
privilege escalation.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-01 17:42:19 -06:00
Flakes Updater
00554ad277 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-02-29 19:53:19 -08:00
Andrew Lytvynov
23fbf0003f clientupdate: handle multiple versions in "apk info tailscale" output (#11310)
The package info output can list multiple package versions, and not in
descending order. Find the newest version in the output, instead of the
first one.

Fixes #11309

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-02-29 11:54:46 -07:00
Irbe Krumina
097c5ed927 util/linuxfw: insert rather than append nftables DNAT rule (#11303)
Ensure that the latest DNATNonTailscaleTraffic rule
gets inserted on top of any pre-existing rules.

Updates tailscale/tailscale#11281

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-29 16:53:43 +00:00
Percy Wegmann
e324a5660f ipn: include full tailfs shares in ipn notifications
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>
2024-02-29 10:16:44 -06:00
Percy Wegmann
80f1cb6227 tailfs: support storing bookmark data on shares
This allows the sandboxed Mac application to store security-
scoped URL bookmarks in order to maintain access to restricted
folders across restarts.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-29 10:16:44 -06:00
Brad Fitzpatrick
f18f591bc6 wgengine: plumb the PeerByKey from wgengine to magicsock
This was just added in 69f4b459 which doesn't yet use it. This still
doesn't yet use it. It just pushes it down deeper into magicsock where
it'll used later.

Updates #7617

Change-Id: If2f8fd380af150ffc763489e1ff4f8ca2899fac6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 19:36:34 -08:00
Andrew Lytvynov
c7474431f1 tsweb: allow empty redirect URL in CleanRedirectURL (#11295)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-02-28 15:57:42 -08:00
Brad Fitzpatrick
b68a09cb34 ipn/ipnlocal: make active IPN sessions keyed by sessionID
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>
2024-02-28 15:16:10 -08:00
Percy Wegmann
2d5d6f5403 ipn,wgengine: only intercept TailFS traffic on quad 100
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)
2024-02-28 17:09:14 -06:00
Ross Zurowski
e83e2e881b client/web: fix Vite CJS deprecation warning (#11288)
Starting in Vite 5, Vite now issues a deprecation warning when using
a CJS-based Vite config file. This commit fixes it by adding the
`"type": "module"` to our package.json to opt our files into ESM module
behaviours.

Fixes #cleanup

Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2024-02-28 16:28:22 -05:00
Brad Fitzpatrick
69f4b4595a wgengine{,/wgint}: add wgint.Peer wrapper type, add to wgengine.Engine
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>
2024-02-28 09:50:18 -08:00
James Tucker
7e17aeb36b .github/workflows: fix regular breakage of go toolchains
This server recently had a common ansible applied, which added a
periodic /tmp cleaner, as is needed on other CI machines to deal with
test tempfile leakage. The setting of $HOME to /tmp means that the go
toolchain in there was regularly getting pruned by the tmp cleaner, but
often incompletely, because it was also in use.

Move HOME to a runner owned directory.

Updates #11248

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-28 08:17:35 -08:00
Brad Fitzpatrick
b4ff9a578f wgengine: rename local variable from 'found' to conventional 'ok'
Updates #cleanup

Change-Id: I799dc86ea9e4a3a949592abdd8e74282e7e5d086
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 07:33:57 -08:00
Brad Fitzpatrick
a8a525282c wgengine: use slices.Clone in two places
Updates #cleanup

Change-Id: I1cb30efb6d09180e82b807d6146f37897ef99307
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 07:33:57 -08:00
Brad Fitzpatrick
74b8985e19 ipn/ipnstate, wgengine: make PeerStatusLite.LastHandshake zero Time means none
... 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>
2024-02-28 07:33:57 -08:00
Andrew Dunham
3dd8ae2f26 net/tstun: fix spelling of "WireGuard"
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ida7e30f4689bc18f5f7502f53a0adb5ac3c7981a
2024-02-28 00:00:18 -05:00
Andrew Dunham
a20e46a80f util/cache: fix missing interface methods (#11275)
Updates #cleanup


Change-Id: Ib3a33a7609530ef8c9f3f58fc607a61e8655c4b5

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2024-02-27 23:03:49 -05:00
Andrew Dunham
23e9447871 tsweb: expose function to generate request IDs
For use in corp.

Updates tailscale/corp#2549

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71debae1ce9ae48cf69cc44c2ab5c443fc3b2005
2024-02-27 18:57:53 -05:00
Mario Minardi
7912d76da0 client/web: update to typescript 5.3.3 (#11267)
Update typescript to 5.3.3. This is a major bump from the previous
version of 4.8.3. This also requires adding newer versions of
@typescript-eslint/eslint-plugin and @typescript-eslint/parser to our
resolutions as eslint-config-react-app pulls in versions that otherwise
do not support typescript 5.x.

eslint-config-react-app has not been updated in 2 years and is seemingly
abandoned, so we may wish to fork it or move to a different eslint config
in the future.

Updates https://github.com/tailscale/corp/issues/17810

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-02-27 14:17:30 -07:00
Andrew Dunham
c5abbcd4b4 wgengine/netstack: add a per-client limit for in-flight TCP forwards
This is a fun one. Right now, when a client is connecting through a
subnet router, here's roughly what happens:

1. The client initiates a connection to an IP address behind a subnet
   router, and sends a TCP SYN
2. The subnet router gets the SYN packet from netstack, and after
   running through acceptTCP, starts DialContext-ing the destination IP,
   without accepting the connection¹
3. The client retransmits the SYN packet a few times while the dial is
   in progress, until either...
4. The subnet router successfully establishes a connection to the
   destination IP and sends the SYN-ACK back to the client, or...
5. The subnet router times out and sends a RST to the client.
6. If the connection was successful, the client ACKs the SYN-ACK it
   received, and traffic starts flowing

As a result, the notification code in forwardTCP never notices when a
new connection attempt is aborted, and it will wait until either the
connection is established, or until the OS-level connection timeout is
reached and it aborts.

To mitigate this, add a per-client limit on how many in-flight TCP
forwarding connections can be in-progress; after this, clients will see
a similar behaviour to the global limit, where new connection attempts
are aborted instead of waiting. This prevents a single misbehaving
client from blocking all other clients of a subnet router by ensuring
that it doesn't starve the global limiter.

Also, bump the global limit again to a higher value.

¹ We can't accept the connection before establishing a connection to the
remote server since otherwise we'd be opening the connection and then
immediately closing it, which breaks a bunch of stuff; see #5503 for
more details.

Updates tailscale/corp#12184

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I76e7008ddd497303d75d473f534e32309c8a5144
2024-02-27 15:25:40 -05:00
Claire Wang
352c1ac96c tailcfg: add latitude, longitude for node location (#11162)
Updates tailscale/corp#17590

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-02-27 15:02:06 -05:00
Irbe Krumina
95dcc1745b cmd/k8s-operator: reconcile tailscale Ingresses when their backend Services change. (#11255)
This is so that if a backend Service gets created after the Ingress, it gets picked up by the operator.

Updates tailscale/tailscale#11251

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <1687799+knyar@users.noreply.github.com>
2024-02-27 15:19:53 +00:00
Irbe Krumina
303125d96d cmd/k8s-operator: configure all proxies with declarative config (#11238)
Containerboot container created for operator's ingress and egress proxies
are now always configured by passing a configfile to tailscaled
(tailscaled --config <configfile-path>.
It does not run 'tailscale set' or 'tailscale up'.
Upgrading existing setups to this version as well as
downgrading existing setups at this version works.

Updates tailscale/tailscale#10869

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-27 15:14:09 +00:00
Irbe Krumina
45d27fafd6 cmd/k8s-operator,k8s-operator,go.{mod,sum},tstest/tools: add Tailscale Kubernetes operator API docs (#11246)
Add logic to autogenerate CRD docs.
.github/workflows/kubemanifests.yaml CI workflow will fail if the doc is out of date with regard to the current CRDs.
Docs can be refreshed by running make kube-generate-all.

Updates tailscale/tailscale#11023

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-27 14:51:53 +00:00
Percy Wegmann
05acf76392 tailfs: fix race condition in tailfs_test
Ues a noop authenticator to avoid potential races in gowebdav's
built-in authenticator.

Fixes #11259

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-27 08:31:04 -06:00
Keli
086ef19439 scripts/installer.sh: auto-start tailscale on Alpine (#11214)
On Alpine, we add the tailscale service but fail to call start.
This means that tailscale does not start up until the user reboots the machine.

Fixes #11161

Signed-off-by: Keli Velazquez <keli@tailscale.com>
2024-02-27 09:17:12 -05:00
Brad Fitzpatrick
1cf85822d0 ipn/ipnstate, wgengine/wgint: add handshake attempts accessors
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
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
eb28818403 wgengine: make pendOpen time later, after dup check
Otherwise on OS retransmits, we'd make redundant timers in Go's timer
heap that upon firing just do nothing (well, grab a mutex and check a
map and see that there's nothing to do).

Updates #cleanup

Change-Id: Id30b8b2d629cf9c7f8133a3f7eca5dc79e81facb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
219efebad4 wgengine: reduce critical section
No need to hold wgLock while using the device to LookupPeer;
that has its own mutex already.

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ib56049fcc7163cf5a2c2e7e12916f07b4f9d67cb
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
9a8c2f47f2 types/key: remove copy returning array by value
It's unnecessary. Returning an array value is already a copy.

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: If7f350b61003ea08f16a531b7b4e8ae483617939
2024-02-26 19:09:12 -08:00
Anton Tolchanov
8cc5c51888 health: warn about reverse path filtering and exit nodes
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>
2024-02-27 00:43:01 +00:00
Nick Khyl
7ef1fb113d cmd/tailscaled, ipn/ipnlocal, wgengine: shutdown tailscaled if wgdevice is closed
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>
2024-02-26 14:45:35 -06:00
Nick Khyl
b42b9817b0 net/dns: do not wait for the interface registry key to appear if the windowsManager is being closed
The WinTun adapter may have been removed by the time we're closing
the dns.windowsManager, and its associated interface registry key might
also have been deleted. We shouldn't use winutil.OpenKeyWait and wait
for the interface key to appear when performing a cleanup as a part of
the windowsManager shutdown.

Updates #11222

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-02-26 14:45:35 -06:00
OSS Updater
82c569a83a go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-02-26 13:26:47 -05:00
Sonia Appasamy
95f26565db client/web: use grants on web UI frontend
Starts using peer capabilities to restrict the management client
on a per-view basis. This change also includes a bulky cleanup
of the login-toggle.tsx file, which was getting pretty unwieldy
in its previous form.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-26 12:59:37 -05:00
Sonia Appasamy
9aa704a05d client/web: restrict serveAPI endpoints to peer capabilities
This change adds a new apiHandler struct for use from serveAPI
to aid with restricting endpoints to specific peer capabilities.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-26 12:59:37 -05:00
Anton Tolchanov
cd9cf93de6 wgengine/netstack: expose TCP forwarder drops via clientmetrics
- add a clientmetric with a counter of TCP forwarder drops due to the
  max attempts;
- fix varz metric types, as they are all counters.

Updates #8210

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-02-26 17:32:34 +00:00
Percy Wegmann
50fb8b9123 tailfs: replace webdavfs with reverse proxies
Instead of modeling remote WebDAV servers as actual
webdav.FS instances, we now just proxy traffic to them.
This not only simplifies the code, but it also allows
WebDAV locking to work correctly by making sure locks are
handled by the servers that need to (i.e. the ones actually
serving the files).

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-26 09:30:22 -06:00
Brad Fitzpatrick
e1bd7488d0 all: remove LenIter, use Go 1.22 range-over-int instead
Updates #11058
Updates golang/go#65685

Change-Id: Ibb216b346e511d486271ab3d84e4546c521e4e22
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-25 12:29:45 -08:00
mrrfv
ff1391a97e net/dns/publicdns: add Mullvad family DNS to the list of known DoH servers
Adds the new Mullvad family DNS server to the known DNS over HTTPS server list.

Signed-off-by: mrrfv <rm-rfv-no-preserve-root@protonmail.com>
2024-02-25 07:51:33 -08:00
Brad Fitzpatrick
6ad6d6b252 wgengine/wglog: add TS_DEBUG_RAW_WGLOG envknob for raw wg logs
Updates #7617 (part of debugging it)

Change-Id: I1bcbdcf0f929e3bcf83f244b1033fd438aa6dac1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-24 14:59:48 -08:00
Brad Fitzpatrick
8b9474b06a wgengine/wgcfg: don't send UAPI to disable keep-alives on new peers
That's already the default. Avoid the overhead of writing it on one
side and reading it on the other to do nothing.

Updates #cleanup (noticed while researching something else)

Change-Id: I449c88a022271afb9be5da876bfaf438fe5d3f58
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-24 14:22:56 -08:00
James Tucker
8d0d46462b net/dns: timeout DOH requests after 10s without response headers
If a client socket is remotely lost but the client is not sent an RST in
response to the next request, the socket might sit in RTO for extended
lengths of time, resulting in "no internet" for users. Instead, timeout
after 10s, which will close the underlying socket, recovering from the
situation more promptly.

Updates #10967

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-23 23:08:12 -08:00
James Tucker
0c5e65eb3f cmd/derper: apply TCP keepalive and timeout to TLS as well
I missed a case in the earlier patch, and so we're still sending 15s TCP
keepalive for TLS connections, now adjusted there too.

Updates tailscale/corp#17587
Updates #3363

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-23 19:19:38 -08:00
James Tucker
c9b6d19fc9 .github/workflows: fix typo in XDG_CACHE_HOME
This appears to be one of the contributors to this CI target regularly
entering a bad state with a partially written toolchain.

Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-23 16:44:29 -08:00
James Tucker
651c4899ac net/interfaces: reduce & cleanup logs on iOS
We don't need a log line every time defaultRoute is read in the good
case, and we now only log default interface updates that are actually
changes.

Updates #3363

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-23 16:37:06 -08:00
Andrea Gottardo
c8c999d7a9 cli/debug: rename DERP debug mode (#11220)
Renames a debug flag in the CLI.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-02-23 15:48:37 -08:00
Mario Minardi
ac281dd493 client/web: update vite and vitest to latest versions (#11200)
Update vite to 5.1.4, and vitest to 1.3.1 (their latest versions). Also
remove vite-plugin-rewrite-all as this is no longer necessary with vite
5.x and has a dependency on vite 4.x.

Updates https://github.com/tailscale/corp/issues/17715

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-02-23 14:50:41 -07:00
Percy Wegmann
15b2c674bf cmd/tailscale: add node attribute instructions to share command help
This adds details on how to configure node attributes to allow
sharing and accessing shares.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-23 13:13:01 -06:00
James Tucker
131f9094fd wgengine/wglog: quieten WireGuard logs for allowedips
An increasing number of users have very large subnet route
configurations, which can produce very large amounts of log data when
WireGuard is reconfigured. The logs don't contain the actual routes, so
they're largely useless for diagnostics, so we'll just suppress them.

Fixes tailscale/corp#17532

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-23 10:06:22 -08:00
Andrew Dunham
e8d2fc7f7f net/tshttpproxy: log when we're using a proxy
Updates #11196

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id6334c10f52f4cfbda9f03dc8096ab7a6c54a088
2024-02-22 19:22:50 -05:00
Mario Minardi
713d2928b1 client/web: update plugin-react-swc to latest version (#11199)
Update plugin-react-swc to the latest version (3.6.0) ahead of updating vite to 5.x.

Updates https://github.com/tailscale/corp/issues/17715

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-02-22 14:00:36 -07:00
Mario Minardi
72140da000 client/web: update vite-plugin-svgr to latest version (#11197)
Update vite-plugin-svgr to the latest version (4.2.0) ahead of updating
vite to 5.x. This is a major version bump from our previous 3.x, and
requires changing the import paths used for SVGs.

Updates https://github.com/tailscale/corp/issues/17715

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-02-22 13:16:44 -07:00
James Tucker
edbad6d274 cmd/derper: add user timeout and reduce TCP keepalive
The derper sends an in-protocol keepalive every 60-65s, so frequent TCP
keepalives are unnecessary. In this tuning TCP keepalives should never
occur for a DERP client connection, as they will send an L7 keepalive
often enough to always reset the TCP keepalive timer. If however a
connection does not receive an ACK promptly it will now be shutdown,
which happens sooner than it would with a normal TCP keepalive tuning.

This re-tuning reduces the frequency of network traffic from derp to
client, reducing battery cost.

Updates tailscale/corp#17587
Updates #3363

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-22 11:22:08 -08:00
Andrea Gottardo
0359c2f94e util/syspolicy: add 'ResetToDefaults' (#11194)
Updates ENG-2133. Adds the ResetToDefaults visibility policy currently only available on macOS, so that the Windows client can read its value.

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-02-22 10:10:31 -08:00
Brad Fitzpatrick
10d130b845 cmd/derper, derp, tailcfg: add admission controller URL option
So derpers can check an external URL for whether to permit access
to a certain public key.

Updates tailscale/corp#17693

Change-Id: I8594de58f54a08be3e2dbef8bcd1ff9b728ab297
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-21 16:57:45 -08:00
Brad Fitzpatrick
2988c1ec52 derp: plumb context to Server.verifyClient
Updates tailscale/corp#17693

Change-Id: If17e02c77d5ad86b820e639176da2d3e61296bae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-21 16:24:44 -08:00
Paul Scott
7708ab68c0 cmd/tailscale/cli: pass "-o 'CanonicalizeHostname no'" to ssh
Fixes #10348

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-02-21 22:34:34 +00:00
Percy Wegmann
91a1019ee2 cmd/testwrapper: apply results of all unit tests to coverage for all packages
This allows coverage from tests that hit multiple packages at once
to be reflected in all those packages' coverage.

Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-21 13:08:17 -06:00
Andrea Gottardo
d756622432 util/syspolicy: add ManagedBy keys for Windows (#11183) 2024-02-20 15:08:06 -08:00
James Tucker
8fe504241d net/ktimeout: add a package to set TCP user timeout
Setting a user timeout will be a more practical tuning knob for a number
of endpoints, this provides a way to set it.

Updates tailscale/corp#17587

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-20 10:49:58 -08:00
Brad Fitzpatrick
a4a909a20b prober: add TLS probe constructor to split dial addr from cert name
So we can probe load balancers by their unique DNS name but without
asking for that cert name.

Updates tailscale/corp#13050

Change-Id: Ie4c0a2f951328df64281ed1602b4e624e3c8cf2e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-19 09:03:13 -08:00
Brad Fitzpatrick
794af40f68 ipn/ipnlocal: remove ancient transition mechanism for https certs
And confusing error message that duplicated the valid cert domains.

Fixes tailscale/corp#15876

Change-Id: I098bc45d83c8d1e0a233dcdf3188869cce66e128
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-17 10:33:11 -08:00
James Tucker
6c3899e6ee logpolicy: allow longer idle log upload connections
From a packet trace we have seen log connections being closed
prematurely by the client, resulting in unnecessary extra TLS setup
traffic.

Updates #3363
Updates tailscale/corp#9230
Updates tailscale/corp#8564

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-16 18:06:09 -08:00
Andrew Dunham
70b7201744 net/dns: fix infinite loop when run on Amazon Linux 2023
This fixes an infinite loop caused by the configuration of
systemd-resolved on Amazon Linux 2023 and how that interacts with
Tailscale's "direct" mode. We now drop the Tailscale service IP from the
OS's "base configuration" when we detect this configuration.

Updates #7816

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I73a4ea8e65571eb368c7e179f36af2c049a588ee
2024-02-16 18:07:32 -05:00
Andrea Gottardo
44e337cc0e tool/gocross: pass flags for visionOS and visionOS Simulator (#11127)
Adds logic in gocross to detect environment variables and pass the right flags so that the backend can be built with the visionOS SDK.

Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-02-16 11:14:17 -08:00
Will Norris
6b582cb8b6 cmd/tailscale: support clickable IPv6 web client addresses
Instead of constructing the `ip:port` string ourselves, use
netip.AddrPortFrom which handles IPv6 correctly.

Updates #11164

Signed-off-by: Will Norris <will@tailscale.com>
2024-02-16 11:00:37 -08:00
Will Norris
24487815e1 cmd/tailscale: make web client URL clickable
Updates #11151

Signed-off-by: Will Norris <will@tailscale.com>
2024-02-16 10:28:34 -08:00
San
69f5664075 ipn/ipnlocal: fix doctor API endpoint (#11155)
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>
2024-02-16 12:17:34 -05:00
Percy Wegmann
3aca29e00e VERSION.txt: this is v1.61.0
Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-15 17:24:49 -06:00
Jason Barnett
4d668416b8 wgengine/router: fix ip rule restoration
Fixes #10857

Signed-off-by: Jason Barnett <J@sonBarnett.com>
2024-02-15 11:36:40 -05:00
Andrew Dunham
52f16b5d10 doctor/ethtool, ipn/ipnlocal: add ethtool bugreport check
Updates #11137

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Idbe862d80e428adb044249c47d9096b87f29d5d8
2024-02-15 10:17:05 -05:00
Patrick O'Doherty
38bba2d23a clientupdate: disable auto update on NixOS (#11136)
Updates #cleanup

NixOS packages are immutable and attempts to update via our tarball
mechanism will always fail as a result. Instead we now direct users to
update their nix channel or nixpkgs flake input to receive the latest
Tailscale release.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-02-14 11:58:29 -08:00
Andrew Dunham
b7104cde4a util/topk: add package containing a probabilistic top-K tracker
This package uses a count-min sketch and a heap to track the top K items
in a stream of data. Tracking a new item and adding a count to an
existing item both require no memory allocations and is at worst
O(log(k)) complexity.

Change-Id: I0553381be3fef2470897e2bd806d43396f2dbb36
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2024-02-14 13:28:58 -05:00
Flakes Updater
7ad2bb87a6 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-02-13 19:39:53 -08:00
Brad Fitzpatrick
61a1644c2a go.mod, all: move away from inet.af domain seized by Taliban
Updates inetaf/tcpproxy#39

Change-Id: I7fee276b116bd08397347c6c949011d76a2842cf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-13 19:21:09 -08:00
Andrew Dunham
b0e96a6c39 net/dns: log more info when openresolv commands fail
Updates #11129

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic594868ba3bc31f6d3b0721ecba4090749a81f7f
2024-02-13 20:48:54 -05:00
Nathan Woodburn
7c0651aea6 scripts/installer.sh: add tuxedoOS to the Ubuntu copies
Signed-off-by: Nathan Woodburn <github@nathan.woodburn.au>
2024-02-13 15:37:15 -08:00
Patrick O'Doherty
256ecd0e8f Revert "tsweb: update ServeMux matching to 1.22.0 syntax (#11090)" (#11125)
This reverts commit 30c9189ed3.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-02-13 10:49:36 -08:00
Aaron Klotz
f7acbefbbb wgengine/router: make the Windows ifconfig implementation reuse existing MibIPforwardRow2 when possible
Looking at profiles, we spend a lot of time in winipcfg.LUID.DeleteRoute
looking up the routing table entry for the provided RouteData.

But we already have the row! We previously obtained that data via the full
table dump we did in getInterfaceRoutes. We can make this a lot faster by
hanging onto a reference to the wipipcfg.MibIPforwardRow2 and executing
the delete operation directly on that.

Fixes #11123

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-02-13 11:17:01 -07:00
Patrick O'Doherty
30c9189ed3 tsweb: update ServeMux matching to 1.22.0 syntax (#11090)
* tsweb: update ServeMux matching to 1.22.0 syntax

Updates #cleanup

Go 1.22.0 introduced the ability to use more expressive routing patterns
that include HTTP method when constructing ServeMux entries.
Applications that attempted to use these patterns in combination with
the old `tsweb.Debugger` would experience a panic as Go would not permit
the use of matching rules with mixed level of specificity. We now
specify the method for each `/debug` handler to prevent
incompatibilities.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-02-13 09:56:00 -08:00
Irbe Krumina
5bd19fd3e3 cmd/k8s-operator,k8s-operator: proxy configuration mechanism via a new ProxyClass custom resource (#11074)
* cmd/k8s-operator,k8s-operator: introduce proxy configuration mechanism via ProxyClass custom resource.

ProxyClass custom resource can be used to specify customizations
for the proxy resources created by the operator.

Add a reconciler that validates ProxyClass resources
and sets a Ready condition to True or False with a corresponding reason and message.
This is required because some fields (labels and annotations)
require complex validations that cannot be performed at custom resource apply time.
Reconcilers that use the ProxyClass to configure proxy resources are expected to
verify that the ProxyClass is Ready and not proceed with resource creation
if configuration from a ProxyClass that is not yet Ready is required.

If a tailscale ingress/egress Service is annotated with a tailscale.com/proxy-class annotation, look up the corresponding ProxyClass and, if it is Ready, apply the configuration from the ProxyClass to the proxy's StatefulSet.

If a tailscale Ingress has a tailscale.com/proxy-class annotation
and the referenced ProxyClass custom resource is available and Ready,
apply configuration from the ProxyClass to the proxy resources
that will be created for the Ingress.

Add a new .proxyClass field to the Connector spec.
If connector.spec.proxyClass is set to a ProxyClass that is available and Ready,
apply configuration from the ProxyClass to the proxy resources created for the Connector.

Ensure that when Helm chart is packaged, the ProxyClass yaml is added to chart templates. Ensure that static manifest generator adds ProxyClass yaml to operator.yaml. Regenerate operator.yaml


Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-13 05:27:54 +00:00
Brad Fitzpatrick
f7f496025a types/views: add test that LenIter doesn't allocate
For a second we thought this was allocating but we were looking
at a CPU profile (which showed calls to mallocgc view makeslice)
instead of the alloc profile.

Updates golang/go#65685 (which if fixed wouldn't have confused us)

Change-Id: Ic0132310d52d8a65758a516142525339aa23b1ed
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-12 18:04:06 -08:00
Percy Wegmann
c42a4e407a tailfs: listen for local clients only on 100.100.100.100
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>
2024-02-12 14:08:00 -06:00
Percy Wegmann
d0ef3a25df cmd/tailscale: hide share subcommand
Fixes #1115

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-12 14:03:01 -06:00
David Anderson
58b8f78e7e flake.nix: build tailscale with go 1.22
Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2024-02-11 20:43:40 -08:00
Maisem Ali
370ecb4654 tailcfg: remove UserProfile.Groups
Removing as per go/group-all-the-things.

Updates tailscale/corp#17445

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-02-11 09:44:11 -08:00
Andrew Dunham
c1c50cfcc0 util/cloudenv: add support for DigitalOcean
Updates #4984

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib229eb40af36a80e6b0fd1dd0cabb07f0d50a7d1
2024-02-10 14:36:20 -05:00
Percy Wegmann
55b372a79f tailscaled: revert to using pointers for subcommands
As part of #10631, we stopped using function pointers for subcommands,
preventing us from registering platform-specific installSystemDaemon
and uninstallSystemDaemon subcommands.

Fixes #11099

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-10 11:55:24 -06:00
Percy Wegmann
87154a2f88 tailfs: fix startup issues on windows
Starts TailFS for Windows too, initializes shares on startup.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-09 22:13:27 -06:00
Percy Wegmann
ddcffaef7a tailfs: disable TailFSForLocal via policy
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>
2024-02-09 20:00:42 -06:00
Percy Wegmann
abab0d4197 tailfs: clean up naming and package structure
- Restyles tailfs -> tailFS
- Defines interfaces for main TailFS types
- Moves implemenatation of TailFS into tailfsimpl package

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-09 20:00:42 -06:00
dependabot[bot]
79b547804b build(deps-dev): bump vite from 4.4.9 to 4.5.2 in /client/web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.9 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 18:42:56 -05:00
James Tucker
24bac27632 util/rands: add Shuffle and Perm functions with on-stack RNG state
The new math/rand/v2 package includes an m-local global random number
generator that can not be reseeded by the user, which is suitable for
most uses without the RNG pools we have in a number of areas of the code
base.

The new API still does not have an allocation-free way of performing a
seeded operations, due to the long term compiler bug around interface
parameter escapes, and the Source interface.

This change introduces the two APIs that math/rand/v2 can not yet
replace efficiently: seeded Perm() and Shuffle() operations. This
implementation chooses to use the PCG random source from math/rand/v2,
as with sufficient compiler optimization, this source should boil down
to only two on-stack registers for random state under ideal conditions.

Updates #17243

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-09 15:19:27 -08:00
Sonia Appasamy
2bb837a9cf client/web: only check policy caps for tagged nodes
For user-owned nodes, only the owner is ever allowed to manage the
node.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-09 18:17:14 -05:00
Andrew Dunham
6f6383f69e .github: fuzzing is now unbroken
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I10dca601c79411b412180a46b3f82136e40544b0
2024-02-09 17:02:58 -05:00
Keisuke Umegaki
7039c06d9b fix toolchain not available error (#11083)
Relates to golang/go#62278
Updates #11058

Signed-off-by: keisku <keisuke.umegaki.630@gmail.com>
2024-02-09 16:46:32 -05:00
Patrick O'Doherty
7c52b27daf Revert "tsweb: update ServeMux matching to 1.22.0 syntax (#11087)" (#11089)
This reverts commit 291f91d164.

Updates #cleanup

This PR needs additional changes to the registration of child handlers under /debug

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-02-09 12:23:49 -08:00
Patrick O'Doherty
291f91d164 tsweb: update ServeMux matching to 1.22.0 syntax (#11087)
Updates #cleanup

Go 1.22.0 introduced the ability to use more expressive routing patterns
that include HTTP method when constructing ServeMux entries.
Applications that attempted to use these patterns in combination with
the old `tsweb.Debugger` would experience a panic as Go would not permit
the use of matching rules with mixed level of specificity.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-02-09 11:27:05 -08:00
Jenny Zhang
c446451bfa cmd/gitops-pusher: only use OAuth creds if non-empty string
`os.LookupEnv` may return true if the variable is present in
the environment but an empty string. We should only attempt
to set OAuth Config if thsoe values are non-empty.

Updates gitops-acl-action#33

Signed-off-by: Jenny Zhang <jz@tailscale.com>
2024-02-09 10:55:59 -05:00
Percy Wegmann
993acf4475 tailfs: initial implementation
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>
2024-02-09 09:13:51 -06:00
Joe Tsai
2e404b769d all: use new AppendEncode methods available in Go 1.22 (#11079)
Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-08 17:55:03 -08:00
Joe Tsai
94a4f701c2 all: use reflect.TypeFor now available in Go 1.22 (#11078)
Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-08 17:34:22 -08:00
Joe Tsai
efddad7d7d util/deephash: cleanup TODO in TestHash (#11080)
Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-08 17:33:25 -08:00
Charlotte Brandhorst-Satzkorn
0f042b9814 cmd/tailscale/cli: fix exit node status output (#11076)
This change fixes the format of tailscale status output when location
based exit nodes are present.

Fixes #11065

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-02-08 14:29:01 -08:00
Andrea Gottardo
6c79f55d48 ipnlocal: force-regen new authURL when it is too old (#10971)
Fixes tailscale/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>
2024-02-08 13:04:01 -08:00
Sonia Appasamy
1217f655c0 cmd/dist: update logs for synology builds
Update logs for synology builds to more clearly callout which variant
is being built. The two existing variants are:

1. Sideloaded (can be manual installed on a device by anyone)
2. Package center distribution (by the tailscale team)

Updates #cleanup

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-08 14:36:55 -05:00
OSS Updater
664b861cd4 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-02-08 10:57:10 -08:00
Will Norris
6f0c5e0c05 client/web: use smart quotes in web UI frontend
add the curly-quotes eslint plugin (same that we use for the admin
panel), and fix existing straight quotes in the current web UI.

Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2024-02-08 10:13:46 -08:00
Will Norris
128c99d4ae client/web: add new readonly mode
The new read-only mode is only accessible when running `tailscale web`
by passing a new `-readonly` flag. This new mode is identical to the
existing login mode with two exceptions:

 - the management client in tailscaled is not started (though if it is
   already running, it is left alone)

 - the client does not prompt the user to login or switch to the
   management client. Instead, a message is shown instructing the user
   to use other means to manage the device.

Updates #10979

Signed-off-by: Will Norris <will@tailscale.com>
2024-02-08 10:11:23 -08:00
License Updater
9f0eaa4464 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-02-08 08:42:15 -08:00
License Updater
78f257d9f8 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-02-08 08:41:12 -08:00
License Updater
5486d8aaf9 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-02-08 08:40:53 -08:00
Irbe Krumina
a6cc2fdc3e cmd/{containerboot,k8s-operator/deploy/manifests}: optionally allow proxying cluster traffic to a cluster target via ingress proxy (#11036)
* cmd/containerboot,cmd/k8s-operator/deploy/manifests: optionally forward cluster traffic via ingress proxy.

If a tailscale Ingress has tailscale.com/experimental-forward-cluster-traffic-via-ingress annotation, configure the associated ingress proxy to have its tailscale serve proxy to listen on Pod's IP address. This ensures that cluster traffic too can be forwarded via this proxy to the ingress backend(s).

In containerboot, if EXPERIMENTAL_PROXY_CLUSTER_TRAFFIC_VIA_INGRESS is set to true
and the node is Kubernetes operator ingress proxy configured via Ingress,
make sure that traffic from within the cluster can be proxied to the ingress target.

Updates tailscale/tailscale#10499

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-08 06:45:42 +00:00
Flakes Updater
2404b1444e go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-02-07 18:18:32 -08:00
Brad Fitzpatrick
c424e192c0 .github/workflows: temporarily disable broken oss-fuzz action
Updates #11064
Updates #11058

Change-Id: I63acc13dece3379a0b2df573afecfd245b7cd6c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-07 18:10:15 -08:00
Brad Fitzpatrick
2bd3c1474b util/cmpx: delete now that we're using Go 1.22
Updates #11058

Change-Id: I09dea8e86f03ec148b715efca339eab8b1f0f644
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-07 18:10:15 -08:00
Brad Fitzpatrick
5ea071186e Dockerfile: use Go 1.22
Updates #11058

Change-Id: I0f63be498be33d71bd90b7956f9fe9666fd7a696
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-07 18:10:15 -08:00
Brad Fitzpatrick
9612001cf4 .github/workflows: update golangci-lint for Go 1.22
Updates #11058

Change-Id: I3785c1f1bea4a4663e7e5fb6d209d3caedae436d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-07 18:10:15 -08:00
Brad Fitzpatrick
b6153efb7d go.mod, README.md: use Go 1.22
Updates #11058

Change-Id: I95eecdc7afe2b5f8189016fdb8a773f78e9f5c42
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-07 18:10:15 -08:00
Tom DNetto
653721541c tsweb: normalize passkey identities in bucketed stats
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: corp#17075
2024-02-07 16:02:36 -08:00
Tom DNetto
8d6d9d28ba tsweb: normalize common StableID's in bucketed stats, export as LabelMap
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: corp#17075
2024-02-07 15:36:39 -08:00
James Tucker
e0762fe331 words: add a list of things you should yahoo!
Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-07 14:47:20 -08:00
James Tucker
0b16620b80 .github/workflows: add privileged tests workflow
We had missed regressions from privileged tests not running, now they
can run.

Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2024-02-07 14:45:22 -08:00
James Tucker
0f5e031133 appc: optimize dns response observation for large route tables
Advertise DNS discovered addresses as a single preference update rather
than one at a time.

Sort the list of observed addresses and use binary search to consult the
list.

Updates tailscale/corp#16636

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-07 14:11:41 -08:00
Andrew Lytvynov
db3776d5bf go.toolchain.rev: bump to Go 1.22.0 (#11055)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-02-07 14:57:57 -07:00
Tom DNetto
af931dcccd tsweb: replace domains/emails in paths when bucketing stats
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: corp#17075
2024-02-07 13:31:59 -08:00
Tom DNetto
36efc50817 tsweb: implementing bucketed statistics for started/finished counts
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: corp#17075
2024-02-06 16:54:17 -08:00
Maisem Ali
b752bde280 types/views: add SliceMapKey[T]
views.Slice are meant to be immutable, and if used as such it
is at times desirable to use them as a key in a map. For non-viewed
slices it was kinda doable by creating a custom key struct but views.Slice
didn't allow for the same so add a method to create that struct here.

Updates tailscale/corp#17122

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-02-06 12:50:28 -08:00
kari-ts
5595b61b96 ipn/localapi: more http status cleanup (#10995)
Use Http.StatusOk instead of 200

Updates #cleanup
2024-02-05 09:34:41 -08:00
Chris Palmer
a633a30711 cmd/hello: link to the Hello KB article (#11022)
Fixes https://github.com/tailscale/corp/issues/17104

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-02-02 15:48:31 -08:00
Joe Tsai
60657ac83f util/deephash: tighten up SelfHasher API (#11012)
Providing a hash.Block512 is an implementation detail of how deephash
works today, but providing an opaque type with mostly equivalent API
(i.e., HashUint8, HashBytes, etc. methods) is still sensible.
Thus, define a public Hasher type that exposes exactly the API
that an implementation of SelfHasher would want to call.
This gives us freedom to change the hashing algorithm of deephash
at some point in the future.

Also, this type is likely going to be called by types that are
going to memoize their own hash results, we additionally add
a HashSum method to simplify this use case.

Add documentation to SelfHasher on how a type might implement it.

Updates: corp#16409

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-01 17:07:41 -08:00
Joe Tsai
84f8311bcd util/deephash: document pathological deephash behavior (#11010)
Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-01 13:49:36 -08:00
James Tucker
ba70cbb930 ipn/ipnlocal: fix app connector route advertisements on exit nodes
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>
2024-02-01 11:56:24 -08:00
James Tucker
e1a4b89dbe appc,ipn/ipnlocal: add app connector routes if any part of a CNAME chain is routed
If any domain along a CNAME chain matches any of the routed domains, add
routes for the discovered domains.

Fixes tailscale/corp#16928

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-01 11:43:07 -08:00
Tom DNetto
2aeef4e610 util/deephash: implement SelfHasher to allow types to hash themselves
Updates: corp#16409
Signed-off-by: Tom DNetto <tom@tailscale.com>
2024-02-01 10:18:03 -08:00
James Tucker
b4b2ec7801 ipn/ipnlocal: fix pretty printing of multi-record peer DNS results
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>
2024-01-31 15:59:17 -08:00
Percy Wegmann
fad6bae764 ipnlocal: log failure to get ssh host keys
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>
2024-01-30 16:57:16 -06:00
Chris Palmer
9744ad47e3 cmd/hello: avoid deprecated apis (#10957)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-29 14:18:49 -08:00
Will Norris
13f8a669d5 cmd/gitops-pusher: fix logic for checking credentials
gitops-pusher supports authenticating with an API key or OAuth
credentials (added in #7393). You shouldn't ever use both of those
together, so we error if both are set.

In tailscale/gitops-acl-action#24, OAuth support is being added to the
GitHub action. In that environment, both the TS_API_KEY and OAuth
variables will be set, even if they are empty values.  This causes an
error in gitops-pusher which expects only one to be set.

Update gitops-pusher to check that only one set of environment variables
are non-empty, rather than just checking if they are set.

Updates #7393

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-29 13:01:29 -08:00
Charlotte Brandhorst-Satzkorn
cce189bde1 words: i like the direction this list is taking
Updates tailscale/corp#14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-01-26 19:42:37 -08:00
Andrew Lytvynov
fbfc3b7e51 cmd/tailscale/cli: run Watch with NotifyNoPrivateKeys (#10950)
When running as non-root non-operator user, you get this error:
```
$ tailscale serve 8080
Access denied: watch IPN bus access denied, must set ipn.NotifyNoPrivateKeys when not running as admin/root or operator

Use 'sudo tailscale serve 8080' or 'tailscale up --operator=$USER' to not require root.
```

It should fail, but the error message is confusing.

With this fix:
```
$ tailscale serve 8080
sending serve config: Access denied: serve config denied

Use 'sudo tailscale serve 8080' or 'tailscale up --operator=$USER' to not require root.
```

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-25 14:59:34 -07:00
James Tucker
0f3b2e7b86 util/expvarx: add a time and concurrency limiting expvar.Func wrapper
expvarx.SafeFunc wraps an expvar.Func with a time limit. On reaching the
time limit, calls to Value return nil, and no new concurrent calls to
the underlying expvar.Func will be started until the call completes.

Updates tailscale/corp#16999
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-24 17:47:16 -08:00
Andrew Dunham
fd94d96e2b net/portmapper: support legacy "urn:dslforum-org" portmapping services
These are functionally the same as the "urn:schemas-upnp-org" services
with a few minor changes, and are still used by older devices. Support
them to improve our ability to obtain an external IP on such networks.

Updates #10911

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I05501fad9d6f0a3b8cf19fc95eee80e7d16cc2cf
2024-01-23 21:29:29 -05:00
Irbe Krumina
75f1d3e7d7 ipn/ipnlocal: fix failing test (#10937)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-23 19:02:07 +00:00
Irbe Krumina
6ee956333f ipn/ipnlocal: fix proxy path that matches mount point (#10864)
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>
2024-01-23 18:12:56 +00:00
Jordan Whited
8b47322acc wgengine/magicsock: implement probing of UDP path lifetime (#10844)
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>
2024-01-23 09:37:32 -08:00
James Tucker
0e2cb76abe appc: add test to ensure that individual IPs are not removed during route updates
If control advised the connector to advertise a route that had already
been discovered by DNS it would be incorrectly removed. Now those routes
are preserved.

Updates tailscale/corp#16833

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-22 17:50:55 -08:00
Charlotte Brandhorst-Satzkorn
ce4553b988 appc,ipn/ipnlocal: optimize preference adjustments when routes update
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>
2024-01-22 17:37:16 -08:00
Irbe Krumina
370ec6b46b cmd/k8s-operator: don't proceed with Ingress that has no valid backends (#10919)
Do not provision resources for a tailscale Ingress that has no valid backends.

Updates tailscale/tailscale#10910

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-22 19:20:23 +00:00
Andrew Dunham
b45089ad85 net/portmapper: handle cases where we have no supported clients
This no longer results in a nil pointer exception when we get a valid
UPnP response with no supported clients.

Updates #10911

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6e3715a49a193ff5261013871ad7fff197a4d77e
2024-01-22 12:46:19 -05:00
James Tucker
4e822c031f go.toolchain.rev: bump Tailscale Go version to 1.21.6
Updates tailscale/go#83

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 18:30:35 -08:00
Flakes Updater
b787c27c00 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-19 18:24:58 -08:00
James Tucker
7e3bcd297e go.mod,wgengine/netstack: bump gvisor
Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 18:23:53 -08:00
David Anderson
17eae5b0d3 tool/gocross: force use of our custom toolchain
The new 'toolchain' directive in go.mod can sometimes force
the use of an upstream toolchain against our wishes. Concurrently,
some of our dependencies have added the 'toolchain' directive, which
transitively adds it to our own go.mod. Force all uses of gocross to
ignore that directive and stick to our customized toolchain.

Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2024-01-19 18:23:21 -08:00
David Anderson
ae79b2e784 tsweb: add a helper to validate redirect URLs
We issue redirects in a few different places, it's time to have
a common helper to do target validation.

Updates tailscale/corp#16875

Signed-off-by: David Anderson <danderson@tailscale.com>
2024-01-19 17:57:54 -08:00
Claire Wang
213d696db0 magicsock: mute noisy expected peer mtu related error (#10870) 2024-01-19 20:04:22 -05:00
kari-ts
62b056d677 VERSION.txt: this is v1.59.0 (#10884)
* VERSION.txt: this is v1.58.0

Signed-off-by: kari-ts <kari@tailscale.com>

* VERSION.txt: this is v1.59.0

---------

Signed-off-by: kari-ts <kari@tailscale.com>
2024-01-19 17:03:50 -08:00
Flakes Updater
5b4eb47300 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-19 16:53:05 -08:00
James Tucker
457102d070 go.mod: bump most deps for start of cycle
Plan9 CI is disabled. 3p dependencies do not build for the target.
Contributor enthusiasm appears to have ceased again, and no usage has
been made.

Skipped gvisor, nfpm, and k8s.

Updates #5794
Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 16:51:39 -08:00
Andrew Dunham
7a0392a8a3 wgengine/netstack: expose gVisor metrics through expvar
When tailscaled is run with "-debug 127.0.0.1:12345", these metrics are
available at:
    http://localhost:12345/debug/metrics

Updates #8210

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I19db6c445ac1f8344df2bc1066a3d9c9030606f8
2024-01-19 18:36:54 -05:00
as2643
832e5c781d util/nocasemaps: add AppendSliceElem method to nocasemaps (#10871)
Updates #7667

Signed-off-by: Anishka Singh <anishkasingh66@gmail.com>
2024-01-19 15:30:12 -08:00
ChandonPierre
2ce596ea7a cmd/k8s-operator/deploy: allow modifying operator tags via Helm values
Updates tailscale/tailscale#10659

Signed-off-by: Chandon Pierre <cpierre@coreweave.com>
2024-01-19 21:22:23 +00:00
Andrew Dunham
2ac7c0161b util/slicesx: add Filter function
For use in corp, where we appear to have re-implemented this in a few
places with varying signatures.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id863a87e674f3caa87945519be8e09650e9c1d76
2024-01-19 11:17:05 -05:00
Irbe Krumina
2aec4f2c43 ./github/workflows/kubemanifests.yaml: fix the paths whose changes should trigger test runs (#10885)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-19 01:51:10 +00:00
James Tucker
8250582fe6 ipn/ipnlocal: make app connector configuration concurrent
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>
2024-01-18 12:26:58 -08:00
James Tucker
38a1cf748a control/controlclient,util/execqueue: extract execqueue into a package
This is a useful primitive for asynchronous execution of ordered work I
want to use in another change.

Updates tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-18 12:08:13 -08:00
Flakes Updater
32f01acc79 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-17 20:06:58 -08:00
James Tucker
24df1ef1ee appc,ipn/ipnlocal,types/appctype: implement control provided routes
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.

Fixes tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-17 14:40:09 -08:00
Andrea Gottardo
543e7ed596 licenses: mention tvOS in apple.md (#10872)
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
2024-01-16 18:32:20 -08:00
License Updater
3eba895293 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-16 15:43:51 -08:00
License Updater
9fa2c4605f licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-16 15:43:14 -08:00
Joe Tsai
c25968e1c5 all: make use of ctxkey everywhere (#10846)
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>
2024-01-16 13:56:23 -08:00
Joe Tsai
7732377cd7 tstime/rate: implement Value.{Marshal,Unmarshal}JSON (#8481)
Implement support for marshaling and unmarshaling a Value.

Updates tailscale/corp#8427

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-01-16 13:48:34 -08:00
Irbe Krumina
1c3c3d6752 cmd/k8s-operator: warn if unsupported Ingress Exact path type is used. (#10865)
To reduce the likelihood of breaking users,
if we implement stricter Exact path type matching in the future.

Updates tailscale/tailscale#10730

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-16 17:02:34 +00:00
Irbe Krumina
50b52dbd7d cmd/k8s-operator: sync StatefulSet labels to their Pods (#10861)
So that users have predictable label values to use when configuring network policies.

Updates tailscale/tailscale#10854

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-16 12:51:10 +00:00
Irbe Krumina
d0492fdee5 cmd/k8s-operator: adds a tailscale IngressClass resource, prints warning if class not found. (#10823)
* cmd/k8s-operator/deploy: deploy a Tailscale IngressClass resource.

Some Ingress validating webhooks reject Ingresses with
.spec.ingressClassName for which there is no matching IngressClass.

Additionally, validate that the expected IngressClass is present,
when parsing a tailscale `Ingress`. 
We currently do not utilize the IngressClass,
however we might in the future at which point
we might start requiring that the right class
for this controller instance actually exists.

Updates tailscale/tailscale#10820

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <anton@tailscale.com>
2024-01-16 12:48:15 +00:00
License Updater
381430eeca licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-13 14:02:42 -08:00
Joe Tsai
241a541864 util/ctxkey: add package for type-safe context keys (#10841)
The lack of type-safety in context.WithValue leads to the common pattern
of defining of package-scoped type to ensure global uniqueness:

	type fooKey struct{}

	func withFoo(ctx context, v Foo) context.Context {
		return context.WithValue(ctx, fooKey{}, v)
	}

	func fooValue(ctx context) Foo {
		v, _ := ctx.Value(fooKey{}).(Foo)
		return v
	}

where usage becomes:

	ctx = withFoo(ctx, foo)
	foo := fooValue(ctx)

With many different context keys, this can be quite tedious.

Using generics, we can simplify this as:

	var fooKey = ctxkey.New("mypkg.fooKey", Foo{})

where usage becomes:

	ctx = fooKey.WithValue(ctx, foo)
	foo := fooKey.Value(ctx)

See https://go.dev/issue/49189

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-01-12 17:35:48 -08:00
kari-ts
c9fd166cc6 net/netmon: when a new network is added, trigger netmon update (#10840)
Fixes #10107
2024-01-12 16:03:04 -08:00
Will Norris
236531c5fc ipn/ipnserver: always allow Windows SYSTEM user to connect
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>
2024-01-12 14:37:53 -08:00
James Tucker
7100b6e721 derp: optimize another per client field alignment
Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:05:39 -08:00
James Tucker
ee20327496 derp: remove unused per-client struct field
Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:05:31 -08:00
OSS Updater
d841ddcb13 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-01-12 16:04:58 -05:00
James Tucker
a7f65b40c5 derp: optimize field order to reduce GC cost
See the field alignment lints for more information.
Reductions are 64->24 and 64->32 respectively.

Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:04:50 -08:00
Charlotte Brandhorst-Satzkorn
e6910974ca cmd/tailscale/cli: add description to exit-node CLI command
This change adds a description to the exit-node CLI command. This
description will be displayed when using `tailscale -h` and `tailscale
exit-node -h`.

Fixes #10787

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-01-12 10:06:09 -08:00
Irbe Krumina
169778e23b cmd/k8s-operator: minor fix in name gen (#10830)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-12 10:08:22 +00:00
Will Norris
b89c113365 client/web: skip connectivity check on https
The manage client always listens on http (non-secure) port 5252.  If the
login client is loaded over https, then the connectivity check to `/ok`
will fail with a mixed-content error. Mixed-content enforcement is a
browser setting that we have no control over, so there's no way around
this.

In this case of the login client being loaded over https, we skip the
connectivity check entirely.  We will always render the sign-in button,
though we don't know for sure if the user has connectivity, so we
provide some additional help text in case they have trouble signing in.

Updates hassio-addons/addon-tailscale#314

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-11 14:51:29 -08:00
James Tucker
ff9c1ebb4a derp: reduce excess goroutines blocking on broadcasts
Observed on one busy derp node, there were 600 goroutines blocked
writing to this channel, which represents not only more blocked routines
than we need, but also excess wake-ups downstream as the latent
goroutines writes represent no new work.

Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-11 14:47:17 -08:00
Irbe Krumina
5cc1bfe82d cmd/k8s-operator: remove configuration knob for Connector (#10791)
The configuration knob (that defaulted to Connector being disabled)
was added largely because the Connector CRD had to be installed in a separate step.
Now when the CRD has been added to both chart and static manifest, we can have it on by default.

Updates tailscale/tailscale#10878

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-11 20:03:53 +00:00
Irbe Krumina
469af614b0 cmd/k8s-operator: fix base truncating for extra long Service names (#10825)
cmd/k8s-operator: fix base truncating for extra long Service names

StatefulSet names for ingress/egress proxies are calculated
using Kubernetes name generator and the parent resource name
as a base.
The name generator also cuts the base, but has a higher max cap.
This commit fixes a bug where, if we get a shortened base back
from the generator, we cut off too little as the base that we
have cut will be passed into the generator again, which will
then itself cut less because the base is shorter- so we end up
with a too long name again.

Updates tailscale/tailscale#10807

Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
2024-01-11 20:02:03 +00:00
Sonia Appasamy
331a6d105f client/web: add initial types for using peer capabilities
Sets up peer capability types for future use within the web client
views and APIs.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-11 11:20:24 -05:00
Andrew Dunham
6540d1f018 wgengine/router: look up absolute path to netsh.exe on Windows
This is in response to logs from a customer that show that we're unable
to run netsh due to the following error:

    router: firewall: adding Tailscale-Process rule to allow UDP for "C:\\Program Files\\Tailscale\\tailscaled.exe" ...
    router: firewall: error adding Tailscale-Process rule: exec: "netsh": cannot run executable found relative to current directory:

There's approximately no reason to ever dynamically look up the path of
a system utility like netsh.exe, so instead let's first look for it
in the System32 directory and only if that fails fall back to the
previous behaviour.

Updates #10804

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I68cfeb4cab091c79ccff3187d35f50359a690573
2024-01-10 20:20:19 -05:00
Irbe Krumina
ca48db0d60 Makefile,build_docker.sh: allow to configure target platform. (#10806)
Build dev tailscale and k8s-operator images for linux/amd64 only by default,
make it possible to configure target build platform via PLATFORM var.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-10 19:19:20 +00:00
Flakes Updater
91c7dfe85c go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-10 09:25:32 -08:00
Andrew Lytvynov
86e476c8d1 version/mkversion: allow version override with $TS_VERSION_OVERRIDE (#10799)
This is useful to build local binaries with custom versions to test
version-specific logic (like updates).

Updates https://github.com/tailscale/corp/issues/16703

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-10 09:03:11 -08:00
Andrew Lytvynov
4ec6a78551 go.mod: update golang-x-crypto fork (#10786)
Pick up a bunch of recent upstream commits.

Updates #8593

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-10 09:02:33 -08:00
Will Norris
84ab040f02 safesocket: detect macsys from within tailscaled
Use the helper method from the version package to detect that we are
running the macsys network extension. This method does the same check
for the HOME environment variable (which works fine in most cases) as
well as the name of the executable (which is needed for the web client).

Updates tailscale/corp#16393

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-10 08:15:40 -08:00
OSS Updater
e7d52eb2f8 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-01-10 10:59:53 -05:00
Irbe Krumina
35f49ac99e cmd/k8s-operator: add Connector CRD to Helm chart and static manifests (#10775)
cmd/k8s-operator: add CRD to chart and static manifest

Add functionality to insert CRD to chart at package time.
Insert CRD to static manifests as this is where they are currently consumed from.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-10 14:20:22 +00:00
Sonia Appasamy
ea9c7f991a cli/set: add printout when web client started
Prints a helpful message with the web UI's address when running
tailscale set --webclient.

Updates tailscale/corp#16345

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-09 17:31:06 -05:00
Rhea Ghosh
4ce33c9758 taildrop: remove breaking abstraction layers for apple (#10728)
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.

Fixes tailscale/corp#14772

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2024-01-09 14:11:34 -06:00
Andrew Lytvynov
7df9af2f5c .github/workflows/govulncheck: migrate to a Github App (#10793)
Send failures to a new channel using a github app token instead of
webhook URL.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-09 11:51:08 -08:00
Andrew Dunham
20f3f706a4 net/netutil: allow 16-bit 4via6 site IDs
The prefix has space for 32-bit site IDs, but the validateViaPrefix
function would previously have disallowed site IDs greater than 255.

Fixes tailscale/corp#16470

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4cdb0711dafb577fae72d86c4014cf623fa538ef
2024-01-09 10:30:46 -05:00
Irbe Krumina
05093ea7d9 cmd/k8s-operator,k8s-operator: allow the operator to deploy exit nodes via Connector custom resource (#10724)
cmd/k8s-operator/deploy/crds,k8s-operator/apis/v1alpha1: allow to define an exit node via Connector CR.

Make it possible to define an exit node to be deployed to a Kubernetes cluster
via Connector Custom resource.

Also changes to Connector API so that one Connector corresponds
to one Tailnet node that can be either a subnet router or an exit
node or both.

The Kubernetes operator parses Connector custom resource and,
if .spec.isExitNode is set, configures that Tailscale node deployed
for that connector as an exit node.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <anton@tailscale.com>
2024-01-09 14:13:22 +00:00
James Tucker
953fa80c6f cmd/{derper,stund},net/stunserver: add standalone stun server
Add a standalone server for STUN that can be hosted independently of the
derper, and factor that back into the derper.

Fixes #8434
Closes #8435
Closes #10745

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-08 16:22:33 -08:00
Will Norris
569b91417f client/web: ensure path prefix has a leading slash
This is simply an extra check to prevent hypothetical issues if a prefix
such as `--prefix="javascript:alert(1)"` was provided.  This isn't
really necessary since the prefix is a configuration flag provided by
the device owner, not user input.  But it does enforce that we are
always interpreting the provided value as a path relative to the root.

Fixes: tailscale/corp#16268

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-08 12:04:02 -08:00
License Updater
e26ee6952f licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-08 11:49:52 -08:00
License Updater
7b113a2d06 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-08 11:47:47 -08:00
Andrew Lytvynov
d96e0a553f tstest/integration: add tests for auto-update defaulting behavior (#10763)
Updates #16244

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-08 09:32:18 -08:00
Sonia Appasamy
55d302b48e client/web: rename Disconnect to Log out
For consistency w/ the CLI command. And to be more accurate to what
is actually happening on this action - node key is expired.

Also updates the disconnected view shown after logout.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-08 12:28:03 -05:00
Irbe Krumina
133699284e cmd/containerboot: add EXPERIMENTAL_TS_CONFIGFILE_PATH env var to allow passing tailscaled config in a file (#10759)
* cmd/containerboot: optionally configure tailscaled with a configfile.

If EXPERIMENTAL_TS_CONFIGFILE_PATH env var is set,
only run tailscaled with the provided config file.
Do not run 'tailscale up' or 'tailscale set'.

* cmd/containerboot: store containerboot accept_dns val in bool pointer

So that we can distinguish between the value being set to
false explicitly bs being unset.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-08 16:14:06 +00:00
Adrian Dewhurst
c05c4bdce4 ipn: apply ControlURL policy before login
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>
2024-01-05 19:58:01 -05:00
Adrian Dewhurst
d50303bef7 docs: add Windows administrative template
To make setting Windows policies easier, this adds ADMX policy
descriptions.

Fixes #6495
Updates ENG-2515

Change-Id: If4613c9d8ec734afec8bd781575e24b4aef9bb73
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-01-05 19:44:19 -05:00
Andrew Dunham
35c303227a net/dns/resolver: add ID to verbose logs in forwarder
To make it easier to correlate the starting/ending log messages.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I2802d53ad98e19bc8914bc58f8c04d4443227b26
2024-01-05 15:25:49 -05:00
Rhea Ghosh
dbe70962b1 taildrop: Allow category Z unicode characters (#10750)
This will expand the unicode character categories that we allow for valid filenames to go from "L, M, N, P, S, and the ASCII space character" to "L, M, N, P, S, Zs"

Fixes #10105

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2024-01-05 12:53:24 -06:00
Andrew Dunham
d3574a350f cmd/tailscale, ipn/ipnlocal: add 'debug dial-types' command
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
2024-01-05 13:42:59 -05:00
Aaron Klotz
aed2cfec4e util/winutil: add some missing docs to restartmgr errors
Just a quick #cleanup.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-01-05 10:12:08 -08:00
Andrew Dunham
46bdbb3878 cmd/tailscaled, tsnet: don't return an interface containing a nil pointer
This tripped me up when I was testing something and wrote:

    if conn != nil {
        conn.Close()
    }

In netstack mode, when an error occurred we were getting a non-nil error
and a non-nil interface that contained a nil pointer. Instead, just
return a nil interface value.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id9ef3dd24529e0e8c53adc60ed914c31fbb10cc4
2024-01-05 11:44:17 -05:00
Andrew Lytvynov
29e98e18f8 ssh/tailssh: use a local error instead of gossh.ErrDenied (#10743)
ErrDenied was added in [our fork of
x/crypto/ssh](acc6f8fe8d)
to short-circuit auth attempts once one fails.

In the case of our callbacks, this error is returned when SSH policy
check determines that a connection should not be allowed. Both
`NoClientAuthCallback` and `PublicKeyHandler` check the policy and will
fail anyway. The `fakePasswordHandler` returns true only if
`NoClientAuthCallback` succeeds the policy check, so it checks it
indirectly too.

The difference here is that a client might attempt all 2-3 auth methods
instead of just `none` but will fail to authenticate regardless.

Updates #8593

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-05 08:02:42 -08:00
James 'zofrex' Sanderson
124dc10261 controlclient,tailcfg,types: expose MaxKeyDuration via localapi (#10401)
Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2024-01-05 12:06:12 +00:00
Andrea Gottardo
d9aeb30281 net/interfaces: handle iOS network transitions (#10680)
Updates #8022
Updates #6075

On iOS, we currently rely on delegated interface information to figure out the default route interface.  The NetworkExtension framework in iOS seems to set the delegate interface only once, upon the *creation* of the VPN tunnel. If a network transition (e.g. from Wi-Fi to Cellular) happens while the tunnel is connected, it will be ignored and we will still try to set Wi-Fi as the default route because the delegated interface is not getting updated as connectivity transitions.

Here we work around this on the Swift side with a NWPathMonitor instance that observes the interface name of the first currently satisfied network path. Our Swift code will call into `UpdateLastKnownDefaultRouteInterface`, so we can rely on that when it is set.

If for any reason the Swift machinery didn't work and we don't get any updates, here we also have some fallback logic: we try finding a hardcoded Wi-Fi interface called en0. If en0 is down, we fall back to cellular (pdp_ip0) as a last resort. This doesn't handle all edge cases like USB-Ethernet adapters or multiple Ethernet interfaces, but it is good enough to ensure connectivity isn't broken.

I tested this on iPhones and iPads running iOS 17.1 and it appears to work. Switching between different cellular plans on a dual SIM configuration also works (the interface name remains pdp_ip0).

Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
2024-01-04 09:40:18 -08:00
James 'zofrex' Sanderson
10c595d962 ipn/ipnlocal: refresh node key without blocking if cap enabled (#10529)
Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
Co-authored-by: Maisem Ali <maisem@tailscale.com>
2024-01-04 17:29:04 +00:00
Irbe Krumina
3a9450bc06 cmd/containerboot: don't parse empty subnet routes (#10738)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-04 12:17:15 +00:00
Irbe Krumina
5a2eb26db3 cmd/containerboot: ensure that subnet routes can be unset. (#10734)
A Tailnet node can be told to stop advertise subnets by passing
an empty string to --advertise-routes flag.
Respect an explicitly passed empty value to TS_ROUTES env var
so that users have a way to stop containerboot acting as a subnet
router without recreating it.
Distinguish between TS_ROUTES being unset and empty.

Updates tailscale/tailscale#10708

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-04 09:17:04 +00:00
Aaron Klotz
e32a064659 cmd/tailscaled: don't create a network monitor in the parent tailscaled on Windows
The service is only used as a watchdog and for piping logs from the child
process. We shouldn't be creating a network monitor in that case.

Fixes #10732

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-01-03 11:57:34 -08:00
Andrew Dunham
fa3639783c net/portmapper: check returned epoch from PMP and PCP protocols
If the epoch that we see during a Probe is less than the existing epoch,
it means that the gateway has either restarted or reset its
configuration, and an existing mapping is no longer valid. Reset any
saved mapping(s) if we detect this case so that a future
createOrGetMapping will not attempt to re-use it.

Updates #10597

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie3cddaf625cb94a29885f7a1eeea25dbf6b97b47
2024-01-03 14:17:50 -05:00
Jordan Whited
b084888e4d wgengine/magicsock: fix typos in docs (#10729)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-01-03 10:50:38 -08:00
Chris Palmer
1f1ab74250 tsweb: use object-src instead of plugin-types (#10719)
plugin-types is deprecated, and setting object-src: 'none' is best
practice. This should result in no functional change.

Fixes #10718

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-03 09:00:57 -08:00
Adrian Dewhurst
3d57c885bf logpolicy: use syspolicy to override LogTarget
Previously, for Windows clients only, a registry value named LogTarget
could override the log server, but only if the environment variable was
unset.

To allow administrators to enforce using a particular log server, switch
this to make the registry value take precedence over the environment
variable, and switch to the newer syspolicy.GetString so that the log
target can be specified by a GPO more easily.

Updates ENG-2515

Change-Id: Ia618986b0e07715d7db4c6df170a24d511c904c9
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-01-03 10:34:35 -05:00
Flakes Updater
1406a9d494 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-02 22:50:03 -08:00
Irbe Krumina
e72f2b7791 go.{mod,sum}: bump mkctr (#10722)
go get github.com/tailscale/mkctr@bf50773ba7349ced8de812c3d5437e8618bd4fa7

Updates tailscale/tailscale#9902

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-03 06:30:13 +00:00
Anton Tolchanov
1d22265f69 release: add shebang to the debian postinst script
Seems like an omission, since we have it in postrm and prerm.

Fixes #10705

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-01-03 02:30:00 +00:00
Chris Palmer
5deeb56b95 cmd/tailscale/cli: document usage more clearly (#10681)
The IP argument is required; only the port is optional.

Updates #10605

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-02 17:43:08 -08:00
Aaron Klotz
5812093d31 util/winutil: publicize existing functions for opening read-only connections to the Windows Service Control Manager
We're going to need to access these from code outside winutil.

Updates #10215

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-12-22 10:52:50 -08:00
Andrew Dunham
cae6edf485 ipn/ipnlocal: fix data race with capForcedNetfilter field
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I1fdad454198d7ea4a898dbff3062818b0db35167
2023-12-21 21:51:09 -05:00
Andrew Lytvynov
2716250ee8 all: cleanup unused code, part 2 (#10670)
And enable U1000 check in staticcheck.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-21 17:40:03 -08:00
Nick Khyl
c9836b454d net/netmon: fix goroutine leak in winMon if the monitor is never started
When the portable Monitor creates a winMon via newOSMon, we register
address and route change callbacks with Windows. Once a callback is hit,
it starts a goroutine that attempts to send the event into messagec and returns.
The newly started goroutine then blocks until it can send to the channel.
However, if the monitor is never started and winMon.Receive is never called,
the goroutines remain indefinitely blocked, leading to goroutine leaks and
significant memory consumption in the tailscaled service process on Windows.
Unlike the tailscaled subprocess, the service process creates but never starts
a Monitor.

This PR adds a check within the callbacks to confirm the monitor's active status,
and exits immediately if the monitor hasn't started.

Updates #9864

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2023-12-21 16:36:52 -06:00
Andrew Lytvynov
2e956713de safesocket: remove ConnectionStrategy (#10662)
This type seems to be a migration shim for TCP tailscaled sockets
(instead of unix/windows pipes). The `port` field was never set, so it
was effectively used as a string (`path` field).
Remove the whole type and simplify call sites to pass the socket path
directly to `safesocket.Connect`.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-21 12:55:14 -08:00
Andrew Lytvynov
1302bd1181 all: cleanup unused code, part 1 (#10661)
Run `staticcheck` with `U1000` to find unused code. This cleans up about
a half of it. I'll do the other half separately to keep PRs manageable.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-20 14:50:30 -08:00
Andrew Dunham
3c333f6341 net/portmapper: add logs about obtained mapping(s)
This logs additional information about what mapping(s) are obtained
during the creation process, including whether we return an existing
cached mapping.

Updates #10597

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9ff25071f064c91691db9ab0b9365ccc5f948d6e
2023-12-20 17:19:08 -05:00
David Crawshaw
f815d66a88 api.md: add docs for setting an IP address
Updates tailscale/corp#16453

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-12-20 12:59:17 -08:00
Andrew Dunham
01286af82b net/interfaces: better handle multiple interfaces in LikelyHomeRouterIP
Currently, we get the "likely home router" gateway IP and then iterate
through all IPs for all interfaces trying to match IPs to determine the
source IP. However, on many platforms we know what interface the gateway
is through, and thus we don't need to iterate through all interfaces
checking IPs. Instead, use the IP address of the associated interface.

This better handles the case where we have multiple interfaces on a
system all connected to the same gateway, and where the first interface
that we visit (as iterated by ForeachInterfaceAddress) isn't also the
default internet route.

Updates #8992

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8632f577f1136930f4ec60c76376527a19a47d1f
2023-12-20 15:33:58 -05:00
Andrew Lytvynov
7a2eb22e94 ipn: remove use of reflect.MethodByName (#10652)
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>
2023-12-20 07:12:26 -08:00
Andrew Dunham
09136e5995 net/netutil: add function to check rp_filter value (#5703)
Updates #4432


Change-Id: Ifc332a5747fc1feffdbb87437308cf8ecb21b0b0

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2023-12-20 00:02:37 -05:00
Paul Scott
65f2d32300 api.md: add device.postureIdentity field
Updates tailscale/corp#15445

Signed-off-by: Paul Scott <paul@tailscale.com>
2023-12-19 22:01:20 +00:00
Paul Scott
03f22cd9fa client/tailscale: add Device.PostureIdentity field
New API fields being added in tailscale/corp#15445.

Updates tailscale/corp#15203

Signed-off-by: Paul Scott <paul@tailscale.com>
2023-12-19 22:01:20 +00:00
Nick Khyl
5e3126f510 tool/gocross: make all Windows DLLs build with static libgcc
In this commit, we have updated the build process for our Windows DLLs
to link statically with libgcc, ensuring our Windows DLLs are self-contained.

Updates #10617

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2023-12-19 15:10:28 -06:00
James Tucker
0957258f84 appc,ipn: prevent undesirable route advertisements
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>
2023-12-19 10:33:25 -08:00
Gavin Greenwalt
865ee25a57 cmd/tailscale/cli: update debug.go (#10644)
redundant run "portmap debugging" word 'debugging'

Signed-off-by: Gavin Greenwalt <gavin@sfstudios.com>
2023-12-19 11:29:52 -05:00
Andrew Dunham
a661287c4b util/cmpx: remove code that's in the stdlib now
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
2023-12-19 09:18:53 -05:00
Andrew Lytvynov
945cf836ee ipn: apply tailnet-wide default for auto-updates (#10508)
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>
2023-12-18 14:57:03 -08:00
Andrew Dunham
d05a572db4 net/portmapper: handle multiple UPnP discovery responses
Instead of taking the first UPnP response we receive and using that to
create port mappings, store all received UPnP responses, sort and
deduplicate them, and then try all of them to obtain an external
address.

Updates #10602

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I783ccb1834834ee2a9ecbae2b16d801f2354302f
2023-12-18 16:02:46 -05:00
Irbe Krumina
38b4eb9419 cmd/k8s-operator/deploy/chart: document passing multiple proxy tags + log level values (#10624)
Updates #cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-18 10:28:06 +00:00
Flakes Updater
dc2792aaee go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-15 22:48:28 -08:00
as2643
3fb6ee7fdb tailscale/logtail: redact public ipv6 and ipv4 ip addresses within tailscaled. (#10531)
Updates #15664

Signed-off-by: Anishka Singh <anishkasingh66@gmail.com>
2023-12-15 15:15:49 -08:00
James Tucker
3a635db06e cmd/connector-gen: add helper tool for wide app connector configurations
connector-gen can initially generate connector ACL snippets and
advertise-routes flags for Github and AWS based on their public IP /
domain data.

Updates ENG-2425
Signed-off-by: James Tucker <james@tailscale.com>
2023-12-15 09:29:42 -08:00
James Tucker
706e30d49e disco: correct noun for nacl box type in disco docs
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-12-14 16:41:53 -08:00
Sonia Appasamy
c6a274611e client/web: use Tailscale IP known by peer node
Throughout the web UI, we present the tailscale addresses for the
self node. In the case of the node being shared out with a user
from another tailnet, the peer viewer may actually know the node
by a different IP than the node knows itself as (Tailscale IPs
can be configured as desired on a tailnet level). This change
includes two fixes:

1. Present the self node's addresses in the frontend as the addresses
   the viewing node knows it as (i.e. the addresses the viewing node
   uses to access the web client).

2. We currently redirect the viewer to the Tailscale IPv4 address if
   viewing it by MagicDNS name, or any other name that maps to the
   Tailscale node. When doing this redirect, which is primarily added
   for DNS rebinding protection, we now check the address the peer
   knows this node as, and redirect to specifically that IP.

Fixes tailscale/corp#16402

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-14 16:36:33 -05:00
Jordan Whited
685b853763 wgengine/magicsock: fix handling of derp.PeerGoneMessage (#10589)
The switch in Conn.runDerpReader() on the derp.ReceivedMessage type
contained cases other than derp.ReceivedPacket that fell through to
writing to c.derpRecvCh, which should only be reached for
derp.ReceivedPacket. This can result in the last/previous
derp.ReceivedPacket to be re-handled, effectively creating a duplicate
packet. If the last derp.ReceivedPacket happens to be a
disco.CallMeMaybe it may result in a disco ping scan towards the
originating peer on the endpoints contained.

The change in this commit moves the channel write on c.derpRecvCh and
subsequent select awaiting the result into the derp.ReceivedMessage
case, preventing it from being reached from any other case. Explicit
continue statements are also added to non-derp.ReceivedPacket cases
where they were missing, in order to signal intent to the reader.

Fixes #10586

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-12-14 12:54:19 -08:00
Andrew Dunham
3ae562366b ipn/ipnlocal: fix usage of slices.Compact
Fixes #10595

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4e96e9c43b8dedb5f88b03368c01b0e46723e15b
2023-12-14 14:11:21 -05:00
Irbe Krumina
1a08ea5990 cmd/k8s-operator: operator can create subnetrouter (#9505)
* k8s-operator,cmd/k8s-operator,Makefile,scripts,.github/workflows: add Connector kube CRD.

Connector CRD allows users to configure the Tailscale Kubernetes operator
to deploy a subnet router to expose cluster CIDRs or
other CIDRs available from within the cluster
to their tailnet.

Also adds various CRD related machinery to
generate CRD YAML, deep copy implementations etc.

Engineers will now have to run
'make kube-generate-all` after changing kube files
to ensure that all generated files are up to date.

* cmd/k8s-operator,k8s-operator: reconcile Connector resources

Reconcile Connector resources, create/delete subnetrouter resources in response to changes to Connector(s).

Connector reconciler will not be started unless
ENABLE_CONNECTOR env var is set to true.
This means that users who don't want to use the alpha
Connector custom resource don't have to install the Connector
CRD to their cluster.
For users who do want to use it the flow is:
- install the CRD
- install the operator (via Helm chart or using static manifests).
For Helm users set .values.enableConnector to true, for static
manifest users, set ENABLE_CONNECTOR to true in the static manifest.

Updates tailscale/tailscale#502


Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-14 13:51:59 +00:00
Chris Palmer
b62a3fc895 client/web: keep redirects on-site (#10525)
Ensure we don't create Location: header URLs that have leading //, which is a
schema-less reference to arbitrary 3rd-party sites. That is, //example.com/foo
redirects off-site, while /example.com/foo is an on-site path URL.

Fixes tailscale/corp#16268

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-12-13 14:28:50 -08:00
Andrew Dunham
727acf96a6 net/netcheck: use DERP frames as a signal for home region liveness
This uses the fact that we've received a frame from a given DERP region
within a certain time as a signal that the region is stil present (and
thus can still be a node's PreferredDERP / home region) even if we don't
get a STUN response from that region during a netcheck.

This should help avoid DERP flaps that occur due to losing STUN probes
while still having a valid and active TCP connection to the DERP server.

RELNOTE=Reduce home DERP flapping when there's still an active connection

Updates #8603

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If7da6312581e1d434d5c0811697319c621e187a0
2023-12-13 16:33:46 -05:00
Andrew Dunham
bac4890467 net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.

Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.

Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.

RELNOTE=Improve UPnP portmapping when multiple UPnP services exist

Updates #8364

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-13 16:32:29 -05:00
Sonia Appasamy
971fa8dc56 VERSION.txt: this is v1.57.0
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-13 15:30:30 -05:00
Flakes Updater
e00141ccbe go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-13 11:30:42 -08:00
OSS Updater
e78cb9aeb3 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-13 12:42:45 -05:00
Sonia Appasamy
4fb679d9cd client/web: fix redirect logic when accessing login client over TS IP
Was previously failing to redirect to the manage client when accessing
the login client with the Tailscale IP.

Updates #10261
Fixes tailscale/corp#16348

Co-authored-by: Will Norris <will@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-13 12:22:06 -05:00
Anton Tolchanov
869b34ddeb prober: log HTTP response body on failure
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-12-13 14:30:16 +00:00
Andrea Barisani
affe11c503 net/netcheck: only run HTTP netcheck for tamago clients
Signed-off-by: Andrea Barisani <andrea@inversepath.com>
2023-12-13 05:28:03 -08:00
Flakes Updater
3ba5fd4baa go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-12 18:38:39 -08:00
OSS Updater
be19262cc5 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-12 21:29:26 -05:00
Charlotte Brandhorst-Satzkorn
7370f3e3a7 words: some stellar additions
Don't get too starry-eyed reviewing these.

Updates tailscale/corp#14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-12-12 17:35:48 -08:00
Sonia Appasamy
77f5d669fa client/web: fix key expiry text when expiry disabled
Displays "No expiry" when disabled.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-12 17:38:35 -05:00
Sonia Appasamy
06af3e3014 client/web: only add cache header for assets
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-12 15:51:22 -05:00
Flakes Updater
808f19bf01 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-12 12:41:09 -08:00
OSS Updater
afe138f18d go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-12 15:38:14 -05:00
Sonia Appasamy
dd0279a6c9 client/web: fix ts connection check
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-12 15:10:30 -05:00
Flakes Updater
8c2fcef453 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-12 11:39:12 -08:00
Sonia Appasamy
343f4e4f26 client/web: refresh auth after syno login
Makes sure we refresh auth state after synology auth has run.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-12 13:41:54 -05:00
Will Norris
1b7d289fad client/web: add debug card to details page
Add a new "Debug" card at the bottom of the details page. It's maybe
premature to add a separate card for this, since all it currently lists
is whether the device is using TUN mode and (for Synology) the DSM
version. But I think it may be helpful to add client connectivity data
(like shown on admin console machine page) as well as a bug report
button.  Those can come soon after the 1.56 launch.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-12 10:18:28 -08:00
OSS Updater
552b1ad094 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-12 11:44:25 -05:00
Sonia Appasamy
1aee6e901d client/web: use prefs.ControlURLOrDefault from controlSupportsCheckMode
To be safe, use `prefs.ControlURLOrDefault()` rather than the current
`prefs.ControlURL` directly.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-12 11:03:58 -05:00
Irbe Krumina
0cdc8e20d6 util/linuxfw: return created chain (#10563)
Ensure that if getOrCreateChain creates a new chain, it actually returns the created chain

Updates tailscale/tailscale#10399

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-12 15:55:02 +00:00
Will Norris
d2fbdb005d client/web: use CSP hash for inline javascript
Calculate and set the hash of the one inline script we have in
index.html. That script is unlikely to change, so hardcoding the hash
seems fine for now.

Updates #10261
Updates tailscale/corp#16266

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-11 20:22:56 -08:00
Sonia Appasamy
bc9b9e8f69 client/web: restrict using an exit node on a couple more platforms
Completed testing of the new UI on the existing platforms that use
it. From testing, QNAP, Unraid, and Home Assistant (in addition to
Synology) all do not play well with using an exit node. For now,
we're disabling this setting from the UI. CLI should be updated to
also disallow selection of an exit node from these platforms.
All platforms still allow for advertising as an exit node.

Co-authored-by: Will Norris <will@tailscale.com>

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-11 20:14:39 -05:00
Will Norris
fc69301fd1 client/web: don't show login button if /ok errors
When displaying the login client, we check for connectivity to the
management client by calling it's /ok handler. If that response is
non-200, then there is something wrong with the management client, so
don't render the login button.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-11 16:54:47 -08:00
Maisem Ali
3aa6468c63 cmd/tailscale/cli: add whois subcommand
Initial implementation of a `tailscale whois` subcommand
which allows users to observe metadata associated with a
Tailscale IP. It also has a `--json` flag to allow consumption
programmatically.

Updates #4217

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-12-12 02:52:11 +05:00
Maisem Ali
fb632036e3 cmd/k8s-operator: drop https:// in capName
Add the new format but keep respecting the old one.

Updates #4217

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-12-12 02:15:18 +05:00
Mario Minardi
4e012794fc client/web: add metric logging when viewing local / remote node (#10555)
Add metric logging for the case where a user is viewing a local or remote
node.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-11 13:50:15 -07:00
Mario Minardi
763b9daa84 client/web: add visual indication for exit node pending approval (#10532)
Add visual indication when running as an exit node prior to receiving
admin approval.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-11 13:40:29 -07:00
Will Norris
e9f203d747 client/web: open new window if iframed
Previously, we were only breaking out of iframes when accessing the
login client over a local IP address (where viewerIdentity is not set).
We need to also handle the case where the user is accessing the login
client over the Tailscale IP, and similarly break out of the iframe when
logging into the management client.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-11 11:41:36 -08:00
OSS Updater
501478dcdc go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-11 13:38:07 -05:00
Will Norris
970dc2a976 client/web: remove 'unsafe-inline' from CSP
I seem to recall I needed this for things to work properly with the vite
dev server, but that doesn't seem to be the case anymore?  Everything
seems to work fine without it.  If we still have issues, we'll need to
look into using a nonce or integrity attribute.

Updates #10261
Fixes tailscale/corp#16266

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-11 10:25:41 -08:00
Sonia Appasamy
c2fe123232 cmd/tailscaled: update ConfigureWebClient's UseSocketOnly value
Previously were always setting `UseSocketOnly` because we were
comparing `args.socketpath != ""`, but `args.socketpath` flag
always gets filled with `paths.DefaultTailscaledSocket()` when
not provided. Rather than comparing to the empty string, compare
to the default value to determine if `UseSocketOnly` should be
set.

Should fix issue with web client being unreachable for Mac App
Store variant of the mac build.

Updates #16054

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-11 12:54:48 -05:00
Mario Minardi
109929d110 client/web: add endpoint for logging device detail click metric (#10505)
Add an endpoint for logging the device detail click metric to allow for
this metric to be logged without having a valid session which is the
case when in readonly mode.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-11 10:50:06 -07:00
Andrew Lytvynov
d8493d4bd5 clientupdate: add explicit Track to Arguments (#10548)
Instead of overloading the Version field, add an explicit Track field.

This fixes a bug where passing a track name in `args.Version` would keep
the track name in `updater.Version` and pass it down the code path to
commands like `apt-get install`. Now, `updater.Version` should always be
a version (or empty string).

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-11 09:20:42 -08:00
Irbe Krumina
1b1b6bb634 ALPINE.txt,Dockerfile{.base},build_docker.sh: bump alpine (#10543)
Bump alpine base image version used to build tailscale/tailscale
and tailscale/k8s-operator images 3.16 -> 3.18

Updates #cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-11 07:03:18 +00:00
License Updater
bac0df6949 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-12-08 21:40:32 -08:00
Will Norris
5a2e6a6f7d client/web: use Home Assistant's X-Ingress-Path header
When running on Home Assistant, use the X-Ingress-Path header to set the
URLPrefix that is passed to the frontend.

Also fix handling of errNotUsingTailscale in the auth handler
(previously it falling through to a later case and returning a 500).
Instead, it's just a terminal state with no auth needed.

Also disable SSH on Home Assistant, since it causes problems on startup
and doesn't make much sense anyway for that platform.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-08 16:36:21 -08:00
Sonia Appasamy
a4c7b0574a client/web: add confirmation dialogs
Add confirmation dialogs for disconnecting and stopping advertisement
of a subnet route.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 19:31:21 -05:00
Will Norris
69b56462fc client/web: check content-type on PATCH requests
Updates #10261
Fixes tailscale/corp#16267

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-08 16:18:18 -08:00
Will Norris
c615fe2296 client/web: add security attributes on session cookie
Limit cookies to HTTP requests (not accessible from javascript).
Set SameSite to "Lax", which is similar to "Strict" but allows for
cookies to be included in requests that come from offsite links.  This
will be necessary when we link to the web client from the admin console.

Updates #10261
Fixes tailscale/corp#16265

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-08 16:18:05 -08:00
Sonia Appasamy
261b6f1e9f client/web: limit updates ui to unstable builds
The updates view still needs a final design pass, limit to unstable
track for now.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 17:57:47 -05:00
Will Norris
33de922d57 client/web: only enforce path prefix in CGI mode
The client has changed a bit since we introduced the path prefix.  It is
now used for two things:

- its original purpose, of ensuring that when the client is run in CGI
  mode at arbitrary paths, then relative paths for assets continue to
  work

- we also now pass the path to the frontend and use wouter to manage
  routes for the various subpages of the client.

When the client is run behind a reverse proxy (as it is in Home
Assistant), it is common for the proxy to rewrite the request so that
the backend application doesn't see the path it's being served at. In
this case, we don't need to call enforcePrefix, since it's already
stripped before it reaches us.  However, wouter (or react router
library) still sees the original path in the browser, and needs to know
what part of it is the prefix that needs to be stripped off.

We're handling this by now only calling enforcePrefix when run in CGI
mode. For Home Assistant, or any other platform that runs the client
behind a reverse proxy with a custom path, they will still need to pass
the `-prefix` flag to `tailscale web`, but we will only use it for route
handling in the frontend.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-08 14:54:22 -08:00
Sonia Appasamy
c2319f0dfa client/web: fix serveAPIAuth in Login mode
In Login mode, must first run system auth. But once authorized,
should be able to reach rest of auth logic to check whether the
user can manage the node. This results in showing/hiding the
sign in button in the frontend login toggle.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 17:42:46 -05:00
Sonia Appasamy
7c172df791 client/web: fix 500 error after logout
Calling DebugPacketFilterRules fails when the node is not logged
in, which was causing 500 errors on the node data endpoint after
logging the node out.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 17:17:50 -05:00
Adrian Dewhurst
86aa0485a6 ipn/ipnlocal, util/syspolicy: make run exit node a preference option
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>
2023-12-08 15:17:21 -05:00
Mario Minardi
21958d2934 client/web: add logging of device management type for web client (#10492)
Add logging of device management type for the web client auth flow. Namely,
this differentiates between viewing a node you do not own, viewing a local
tagged node, viewing a remote tagged node, managing a local node, and
managing a remote node.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-08 13:15:57 -07:00
Maisem Ali
7bdea283bd cmd/containerboot: symlink TS_SOCKET to socket expected by CLI
`tailscaled` and `tailscale` expect the socket to be at
`/var/run/tailscale/tailscaled.sock`, however containerboot
would set up the socket at `/tmp/tailscaled.sock`. This leads to a
poor UX when users try to use any `tailscale` command as they
have to prefix everything with `--socket /tmp/tailscaled.sock`.

To improve the UX, this adds a symlink to
`/var/run/tailscale/tailscaled.sock` to point to `/tmp/tailscaled.sock`.

This approach has two benefits, 1 users are able to continue to use
existing scripts without this being a breaking change. 2. users are
able to use the `tailscale` CLI without having to add the `--socket` flag.

Fixes tailscale/corp#15902
Fixes #6849
Fixes #10027

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-12-08 11:28:21 -08:00
Sonia Appasamy
ddb4b51122 client/web: always run platform auth for login mode
Even if connected to the login client over tailscale, still check
platform auth so the browser can obtain the tokens it needs to make
platform requests complete successfully.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 13:42:10 -05:00
Andrew Lytvynov
e25f114916 ipn,cmd/tailscale/cli: support hierarchical MaskedPrefs (#10507)
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>
2023-12-08 10:19:25 -08:00
Flakes Updater
2f01d5e3da go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-08 10:16:03 -08:00
OSS Updater
6389322619 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-08 13:11:52 -05:00
Aaron Klotz
0f646937e9 clientupdate: remove TS_NOLAUNCH and GUI restart hacks from autoupdate
We've fixed the underlying issue in github.com/tailscale/corp/issues/13998.

Fixes #10513

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-12-08 10:07:45 -08:00
Andrea Gottardo
646d17ac8d util/syspolicy: rename client metric keys (#10516)
Updates ENG-2513. Renames client metrics keys used on Windows for consistency with Apple platforms.

Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
2023-12-08 09:51:24 -08:00
Sonia Appasamy
d5d42d0293 client/web: small UI cleanups
Updates:
* Card component used throughout instead of custom card class
* SSH toggle changed to non-editable text/status icon in readonly
* Red error text on subnet route input when route post failed

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-08 12:45:09 -05:00
Sonia Appasamy
e5e5ebda44 client/web: precompress assets
Precompress webclient assets with precompress util. This cuts our
css and js build sizes to about 1/3 of non-compressed size. Similar
compression done on tsconnect and adminhttp assets.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-07 20:57:31 -05:00
Sonia Appasamy
97f8577ad2 client/web: restructure api mutations into hook
This commit makes some restructural changes to how we handle api
posting from the web client frontend.

Now that we're using SWR, we have less of a need for hooks like
useNodeData that return a useSWR response alongside some mutation
callbacks. SWR makes it easy to mutate throughout the UI without
needing access to the original data state in order to reflect
updates. So, we can fetch data without having to tie it to post
callbacks that have to be passed around through components.

In an effort to consolidate our posting endpoints, and make it
easier to add more api handlers cleanly in the future, this change
introduces a new `useAPI` hook that returns a single `api` callback
that can make any changes from any component in the UI. The hook
itself handles using SWR to mutate the relevant data keys, which
get globally reflected throughout the UI.

As a concurrent cleanup, node types are also moved to their own
types.ts file, to consolidate data types across the app.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-07 18:32:32 -05:00
Andrew Dunham
9fd29f15c7 util/cache: add package for general-purpose caching
This package allows caching arbitrary key/value pairs in-memory, along
with an interface implemented by the cache types.

Extracted from #7493

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic8ca820927c456721cf324a0c8f3882a57752cc9
2023-12-07 18:19:38 -05:00
Adrian Dewhurst
f706a3abd0 ipn/ipnlocal, util/syspolicy: add auto update policy
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>
2023-12-07 17:29:22 -05:00
Sonia Appasamy
ef4f1e3a0b client/web: add loading state to app
Displays animated loading dots while initial auth and data endpoints
are fetching.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-07 17:08:15 -05:00
Andrew Dunham
3f576fc4ca ci: run 'go vet' in golangci-lint; fix errors in tests
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ice78fc907bad24c1de749a1595e212ef2db4b8bb
2023-12-07 15:08:28 -05:00
Mario Minardi
f5f21c213c client/web: add additional web client metrics logging (#10462)
Add additional web client metric logging. Namely, add logging events for
auth / deauth, enable / disable using exit node, enable / disable SSH,
enable / disable advertise routes, and click events on the device details
button.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-07 09:24:25 -07:00
Naman Sood
97f84200ac wgengine/router: implement UpdateMagicsockPort for CallbackRouter (#10494)
Updates #9084.

Signed-off-by: Naman Sood <mail@nsood.in>
2023-12-07 10:45:14 -05:00
Sonia Appasamy
95655405b8 client/web: start using swr for some fetching
Adds swr to the web client, and starts by using it from the
useNodeData hook.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 21:20:13 -05:00
Sonia Appasamy
014ae98297 client/web: style tweaks
Style changes made in live pairing session.

Updates #10261

Co-authored-by: Will Norris <will@tailscale.com>
Co-authored-by: Alessandro Mingione <alessandro@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 17:31:53 -05:00
Adrian Dewhurst
1a4d423328 ipn/ipnlocal: add additional syspolicy enforcement
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>
2023-12-06 16:34:36 -05:00
Sonia Appasamy
2731a9da36 client/web: fix exit node selector styling
Remove padding on top of search bar, remove rounded corners of
bottom border of earch bar, and add auto focus.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 16:19:52 -05:00
Adrian Dewhurst
af32d1c120 ipn/ipnlocal: better enforce system policies
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.

Fixes tailscale/corp#15585

Change-Id: Ide4c3a4b00a64e43f506fa1fab70ef591407663f
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-12-06 14:45:06 -05:00
Sonia Appasamy
ac6f671c54 ipn/localapi: use clientupdate.CanAutoUpdate from serveUpdateCheck
Fixes #10486

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 14:08:37 -05:00
Sonia Appasamy
a54a4f757b client/web: add licenses and policies links
Adds a footer to the device details page that mirrors license and
policy content on other Tailscale clients.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 13:44:52 -05:00
Sonia Appasamy
cc6729a0bc .github/workflows: add webclient workflow
Add workflow to run yarn lint/test/format-check against the web
client on pull requests.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-06 13:21:27 -05:00
Mario Minardi
4a24db852a client/web: use IPv4 instead of IP in login view (#10483)
The IP property in node data was renamed to IPv4 but refactoring the usage
of the property was missed in this file.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-06 10:08:23 -07:00
Denton Gentry
137e9f4c46 net/portmap: add test of Mikrotik Root Desc XML.
Unfortunately in the test we can't reproduce the failure seen
in the real system ("SOAP fault: UPnPError")

Updates https://github.com/tailscale/tailscale/issues/8364

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-12-05 22:10:27 -08:00
Naman Sood
d46a4eced5 util/linuxfw, wgengine: allow ingress to magicsock UDP port on Linux (#10370)
* util/linuxfw, wgengine: allow ingress to magicsock UDP port on Linux

Updates #9084.

Currently, we have to tell users to manually open UDP ports on Linux when
certain firewalls (like ufw) are enabled. This change automates the process of
adding and updating those firewall rules as magicsock changes what port it
listens on.

Signed-off-by: Naman Sood <mail@nsood.in>
2023-12-05 18:12:02 -05:00
Andrew Lytvynov
aad5fb28b1 go.toolchain.rev: bump to 1.21.5 (#10475)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-05 16:16:33 -06:00
Claire Wang
47db67fef5 util/syspolicy: add policy counters (#10471)
Fixes tailscale/corp#16138

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-12-05 17:13:05 -05:00
Sonia Appasamy
a95b3cbfa8 client/web: add copyable components throughout UI
Updates the IP address on home view to open a copyable list of node
addresses on click. And makes various values on the details view
copyable text items, mirroring the machine admin panel table.

As part of these changes, pulls the AddressCard, NiceIP and QuickCopy
components from the admin panel, with the AddressCard slightly modified
to avoid needing to also pull in the CommandLine component.

A new toaster interface is also added, allowing us to display success
and failure toasts throughout the UI. The toaster code is slightly
modified from it's admin form to avoid the need for some excess
libraries.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-05 16:52:19 -05:00
Naman Sood
650c67a0a1 tailcfg: bump CapabilityVersion for Linux netfilter NodeAttrs and c2n endpoint
Updates tailscale/corp#14029.

Signed-off-by: Naman Sood <mail@nsood.in>
2023-12-05 14:22:02 -05:00
Naman Sood
0a59754eda linuxfw,wgengine/route,ipn: add c2n and nodeattrs to control linux netfilter
Updates tailscale/corp#14029.

Signed-off-by: Naman Sood <mail@nsood.in>
2023-12-05 14:22:02 -05:00
James Tucker
215f657a5e wgengine/router: create netfilter runner in setNetfilterMode
This will enable the runner to be replaced as a configuration side
effect in a later change.

Updates tailscale/corp#14029

Signed-off-by: James Tucker <james@tailscale.com>
2023-12-05 14:22:02 -05:00
Adrian Dewhurst
94a64c0017 util/syspolicy: rename incorrectly named policy keys
These keys were intended to match the Apple platforms, but accidentally
used the wrong name.

Updates ENG-2133

Change-Id: I9ed7a17919e34e2d8896a5c64efc4d0c0003166e
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-12-05 13:58:31 -05:00
License Updater
70f201c691 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-12-05 09:47:02 -08:00
License Updater
9095518c2d licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-12-05 09:46:24 -08:00
Matt Layher
a217f1fccf all: fix nilness issues
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2023-12-05 11:43:14 -05:00
Will Norris
c5208f8138 client/web: small tweaks for small screens
Add left and right padding around entire client so that the cards don't
run into the side of the screen. Also tighten up vertical spacing in
couple of places.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-05 08:05:09 -08:00
Andrew Dunham
c4ccdd1bd1 net/interfaces: ensure we return valid 'self' IP in LikelyHomeRouterIP
Before this fix, LikelyHomeRouterIP could return a 'self' IP that
doesn't correspond to the gateway address, since it picks the first
private address when iterating over the set interfaces as the 'self' IP,
without checking that the address corresponds with the
previously-detected gateway.

This behaviour was introduced by accident in aaf2df7, where we deleted
the following code:

    for _, prefix := range privatev4s {
        if prefix.Contains(gateway) && prefix.Contains(ip) {
            myIP = ip
            ok = true
            return
        }
    }

Other than checking that 'gateway' and 'ip' were private IP addresses
(which were correctly replaced with a call to the netip.Addr.IsPrivate
method), it also implicitly checked that both 'gateway' and 'ip' were a
part of the *same* prefix, and thus likely to be the same interface.

Restore that behaviour by explicitly checking pfx.Contains(gateway),
which, given that the 'ip' variable is derived from our prefix 'pfx',
ensures that the 'self' IP will correspond to the returned 'gateway'.

Fixes #10466

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iddd2ee70cefb9fb40071986fefeace9ca2441ee6
2023-12-05 10:29:37 -05:00
Mario Minardi
6b083a8ddf client/web: add metric logging logic to the web client (#10434)
Add metric logging logic for the web client frontend. This is an initial
pass of adding the base logic, plus a single point where it is used for
validation that the logging is working correctly. More metric logging
calls will follow in subsquent PRs.

Updates https://github.com/tailscale/tailscale/issues/10261

Signed-off-by: Mario Minardi <mario@tailscale.com>
2023-12-05 08:28:19 -07:00
Will Norris
9c4b73d77d client/web: handle login client inside an iframe
If the login client is inside an iframe, open the management client in a
new window, since it can't be loaded in the frame.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-04 14:28:51 -08:00
Will Norris
9441a4e15d client/web: render 404 message in empty card
Switch the "feature disabled" page to use the same treatment.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-04 14:04:15 -08:00
Sonia Appasamy
65643f6606 client/web: update device and connected icon
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-04 16:39:56 -05:00
Will Norris
f5989f317f client/web: handle offline exit nodes
If the currently selected exit node is offline, render the exit node
selector in red with an error message. Update exit nodes in the dropdown
to indicate if they are offline, and don't allow them to be selected.

This also updates some older color values to use the new colors.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-04 13:31:05 -08:00
Sonia Appasamy
b144391c06 client/web: add cancel button to subnet router input section
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-04 16:28:28 -05:00
Sonia Appasamy
95e9d22a16 client/web: button, link, and other small UI updates
Makes the following changes:
* Use “link” class in various spots
* Remove button appearance on Exit Node dropdown in readonly mode
* Update `-stone-` colors to `-gray-` (couple spots missed by
  original color config commit)
* Pull full ui/button component from admin panel, and update
  buttons throughout UI to use this component
* Remove various buttons in readonly view to match mocks
* Add route (and “pending approval”) highlights to Subnet router
  settings card
* Delete legacy client button styles from index.css
* Fix overflow of IPv6 address on device details view

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-04 15:50:29 -05:00
Aaron Klotz
64a26b221b net/dns: use an additional registry setting to disable dynamic DNS updates for our interface on Windows
Fixes #9775

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-12-04 09:10:35 -08:00
Irbe Krumina
49fd0a62c9 cmd/k8s-operator: generate static kube manifests from the Helm chart. (#10436)
* cmd/k8s-operator: generate static manifests from Helm charts

This is done to ensure that there is a single source of truth
for the operator kube manifests.
Also adds linux node selector to the static manifests as
this was added as a default to the Helm chart.

Static manifests can now be generated by running
`go generate tailscale.com/cmd/k8s-operator`.

Updates tailscale/tailscale#9222

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-04 10:18:07 +00:00
Andrew Lytvynov
263e01c47b wgengine/filter: add protocol-agnostic packet checker (#10446)
For use in ACL tests, we need a way to check whether a packet is allowed
not just with TCP, but any protocol.

Updates #3561

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-02 16:30:33 -06:00
Flakes Updater
c85532270f go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-01 20:20:03 -08:00
Adrian Dewhurst
2003d1139f go.mod: update certstore
Updates tailscale/coral#118

Change-Id: Ie535ab890f95d13d050b2acc7d4ad1e3f8316877
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-12-01 23:14:02 -05:00
Will Norris
f9550e0bed client/web: indicate if ACLs prevent access
Use the packet filter rules to determine if any device is allowed to
connect on port 5252.  This does not check whether a specific device can
connect (since we typically don't know the source device when this is
used).  Nor does it specifically check for wide-open ACLs, which is
something we may provide a warning about in the future.

Update the login popover content to display information when the src
device is unable to connect to the dst device over its Tailscale IP. If
we know it's an ACL issue, mention that, otherwise list a couple of
things to check. In both cases, link to a placeholder URL to get more
information about web client connection issues.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-12-01 16:51:12 -08:00
Sonia Appasamy
5e125750bc client/web: center and fix height of header
Centers login pill with Tailscale icon, and fixes height of login
pill.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-01 16:05:36 -08:00
OSS Updater
f13255d54d go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-12-01 17:32:44 -05:00
Sonia Appasamy
7a4ba609d9 client/web: show features based on platform support
Hiding/disabling UI features when not available on the running
client.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-01 17:01:13 -05:00
Sonia Appasamy
7d61b827e8 client/web: adjust colors and some UI margins
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-01 15:41:57 -05:00
Sonia Appasamy
b155c7a091 client/web: move postcss config into package.json
A little cleanup.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-01 12:21:49 -05:00
Aaron Klotz
db39a43f06 util/winutil: add support for restarting Windows processes in specific sessions
This PR is all about adding functionality that will enable the installer's
upgrade sequence to terminate processes belonging to the previous version,
and then subsequently restart instances belonging to the new version within
the session(s) corresponding to the processes that were killed.

There are multiple parts to this:

* We add support for the Restart Manager APIs, which allow us to query the
  OS for a list of processes locking specific files;
* We add the RestartableProcess and RestartableProcesses types that query
  additional information about the running processes that will allow us
  to correctly restart them in the future. These types also provide the
  ability to terminate the processes.
* We add the StartProcessInSession family of APIs that permit us to create
  new processes within specific sessions. This is needed in order to
  properly attach a new GUI process to the same RDP session and desktop that
  its previously-terminated counterpart would have been running in.
* I tweaked the winutil token APIs again.
* A lot of this stuff is pretty hard to test without a very elaborate
  harness, but I added a unit test for the most complicated part (though it
  requires LocalSystem to run).

Updates https://github.com/tailscale/corp/issues/13998

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-11-30 14:04:27 -08:00
Andrew Lytvynov
59d1077e28 clientupdate: cleanup tailscale binary copies on Windows (#10433)
When updating on Windows, we make a copy of the tailscale.exe file in a
temp directory to perform the update, because the original tailscale.exe
gets deleted during the update.

This can eat up disk space if a machine is stuck doing repeated failed
update attempts. Clean up old copies explicitly before making a new one,
same as we do with MSIs.

Updates #10082

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-30 12:22:29 -08:00
Marwan Sulaiman
b819f66eb1 tsweb: propagate RequestID via context and entire request
The recent addition of RequestID was only populated if the
HTTP Request had returned an error. This meant that the underlying
handler has no access to this request id and any logs it may have
emitted were impossible to correlate to that request id. Therefore,
this PR adds a middleware to generate request ids and pass them
through the request context. The tsweb.StdHandler automatically
populates this request id if the middleware is being used. Finally,
inner handlers can use the context to retrieve that same request id
and use it so that all logs and events can be correlated.

Updates #2549

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-11-30 15:00:29 -05:00
Juergen Knaack
c27aa9e7ff net/dns: fix darwin dns resolver files
putting each nameserver on one line in /etc/resolver/<domain>

fixes: #10134
Signed-off-by: Juergen Knaack <jk@jk-1.de>
2023-11-29 19:25:31 -08:00
Sonia Appasamy
cbd0b60743 client/web: remove ControlAdminURL override
Was setting this for testing, snuck into the merged version.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-29 18:34:12 -05:00
Sonia Appasamy
bcc9b44cb1 client/web: hide admin panel links for non-tailscale control servers
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-29 16:51:46 -05:00
Claire Wang
8af503b0c5 syspolicy: add exit node related policies (#10172)
Adds policy keys ExitNodeID and ExitNodeIP.
Uses the policy keys to determine the exit node in preferences.
Fixes tailscale/corp#15683

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-11-29 16:48:25 -05:00
Sonia Appasamy
ecd1ccb917 client/web: add subnet routes view
Add UI view for mutating the node's advertised subnet routes.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-29 15:09:07 -05:00
Sonia Appasamy
7aa981ba49 client/web: remove duplicate WhoIs call
Fixes a TODO in web.authorizeRequest.

`getSession` calls `WhoIs` already. Call `getSession` earlier in
`authorizeRequest` so we can avoid the duplicate `WhoIs` check on
the same request.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-29 14:46:57 -05:00
Sonia Appasamy
bc4e303846 ipn/ipnstate: add AllowedIPs to PeerStatus
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>
2023-11-29 14:35:30 -05:00
Andrew Lytvynov
ac4b416c5b cmd/tailscale,ipn/ipnlocal: pass available update as health message (#10420)
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>
2023-11-29 09:55:39 -08:00
Will Norris
26db9775f8 client/web: skip check mode for non-tailscale.com control servers (#10413)
client/web: skip check mode for non-tailscale.com control servers

Only enforce check mode if the control server URL ends in
".tailscale.com".  This allows the web client to be used with headscale
(or other) control servers while we work with the project to add check
mode support (tracked in juanfont/headscale#1623).

Updates #10261

Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Will Norris <will@tailscale.com>
2023-11-29 08:44:48 -08:00
Sonia Appasamy
ab0e25beaa client/web: fix Vite dev server build error
6e30c9d1f added eslint to the web client. As a part of that change,
the existing yarn.lock file was removed and yarn install run to build
with a clean yarn dependencies set with latest versions. This caused
a change in the "vite-plugin-rewrite-all" package that fails at build
time with our existing vite config. This is a known bug with some
suggested fixes:
https://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only

Rather than editing our package.json type, this commit reverts back
the yarn.lock file to it's contents at the commit just before 6e30c9d1f
and then only runs yarn install to add the new eslint packages, rather
than installing the latest versions of all packages.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-29 10:54:05 -05:00
Tom DNetto
8a660a6513 ipn/ipnlocal: update hostinfo when app connector state is toggled
Updates: https://github.com/tailscale/corp/issues/16041
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-11-28 16:08:13 -08:00
Sonia Appasamy
6e30c9d1fe client/web: add eslint
Add eslint to require stricter typescript rules, particularly around
required hook dependencies. This commit also updates any files that
were now throwing errors with eslint.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-28 17:06:33 -05:00
Andrew Lytvynov
5a9e935597 clientupdate: implement update for Unraid (#10344)
Use the [`plugin`
CLI](https://forums.unraid.net/topic/72240-solved-is-there-a-way-to-installuninstall-plugins-from-script/#comment-676870)
to fetch and apply the update.

Updates https://github.com/tailscale/tailscale/issues/10184

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-28 13:28:30 -08:00
Jordan Whited
5e861c3871 wgengine/netstack: disable RACK on Windows (#10402)
Updates #9707

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-28 12:12:32 -08:00
Sonia Appasamy
5f40b8a0bc scripts/check_license_headers: enforce license on ts/tsx files
Enforcing inclusion of our OSS license at the top of .ts and .tsx
files. Also updates any relevant files in the repo that were
previously missing the license comment. An additional `@license`
comment is added to client/web/src/index.tsx to preserve the
license in generated Javascript.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-28 13:57:09 -05:00
Jenny Zhang
09b4914916 tka: clarify field comment
Updates #cleanup
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2023-11-27 18:35:33 -05:00
Denton Gentry
67f3b2a525 tsnet: add CapturePcap method for debugging
Application code can call the tsnet s.CapturePcap(filename) method
to write all packets, sent and received, to a pcap file. The cleartext
packets are written, outside the Wireguard tunnel. This is expected
to be useful for debugging.

Updates https://github.com/tailscale/tailscale/issues/9707

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-11-27 12:46:28 -08:00
Sonia Appasamy
b247435d66 client/web: scroll exit node dropdown to top on search
When search input changes, reset the scroll to the top of the
dropdown list.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-27 12:43:10 -05:00
Jenny Zhang
d5d84f1a68 cmd/tailscale: also warn about IP forwarding if using CLI set
We warn users about IP forwarding being disabled when using
`--avertise-routes` in `tailscale up`, this adds the same warnings
to `tailscale set`.

Updates tailscale/corp#9968
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2023-11-24 16:27:52 -05:00
Irbe Krumina
18ceb4e1f6 cmd/{containerboot,k8s-operator}: allow users to define tailnet egress target by FQDN (#10360)
* cmd/containerboot: proxy traffic to tailnet target defined by FQDN

Add a new Service annotation tailscale.com/tailnet-fqdn that
users can use to specify a tailnet target for which
an egress proxy should be deployed in the cluster.

Updates tailscale/tailscale#10280

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-24 16:24:48 +00:00
David Anderson
2a01df97b8 flake.nix: use vendorHash instead of vendorSha256
vendorSha256 is getting retired, and throws warning in builds.

Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-11-23 20:32:54 -08:00
Flakes Updater
6b395f6385 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-23 20:16:43 -08:00
Charlotte Brandhorst-Satzkorn
9e63bf5fd6 words: crikey! what a beauty of a list
If I have to add a tail, or a scale, mate, I will add it.

Updates tailscale/corp#14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-11-22 21:17:05 -08:00
Tom DNetto
611e0a5bcc appc,ipn/local: support wildcard when matching app-connectors
Updates: ENG-2453
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-11-22 14:47:44 -08:00
Jordan Whited
1af7f5b549 wgengine/magicsock: fix typo in Conn.handlePingLocked() (#10365)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-22 14:33:12 -08:00
Andrew Dunham
5aa7687b21 util/httpm: don't run test if .git doesn't exist
Updates #9635

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9089200f9327605036c88fc12834acece0c11694
2023-11-22 12:09:59 -05:00
Claire Wang
afacf2e368 containerboot: Add TS_ACCEPT_ROUTES (#10176)
Fixes tailscale/corp#15596

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-11-22 11:45:44 -05:00
Gabriel Martinez
128d3ad1a9 cmd/k8s-operator: helm chart add missing keys (#10296)
* cmd/k8s-operator: add missing keys to Helm values file

Updates  #10182

Signed-off-by: Gabriel Martinez <gabrielmartinez@sisti.pt>
2023-11-22 11:02:54 +00:00
Jordan Whited
e1d0d26686 go.mod: bump wireguard-go (#10352)
This pulls in tailscale/wireguard-go@8cc8b8b and
tailscale/wireguard-go@cc193a0, which improve throughput and latency
under load.

Updates tailscale/corp#11061

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-21 11:21:39 -08:00
Cole Helbling
a8647b3c37 flake: fixup version embedding (#9997)
It looks like `gitCommitStamp` is the new "entrypoint" for setting this
information.

Fixes #9996.

Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-11-21 12:49:34 -05:00
Percy Wegmann
17501ea31a ci: report test coverage to coveralls.io
This records test coverage for the amd64 no race tests and uploads the
results to coveralls.io.

Updates #cleanup

Signed-off-by: Ox Cart <ox.to.a.cart@gmail.com>
2023-11-21 09:08:37 -06:00
Irbe Krumina
66471710f8 cmd/k8s-operator: truncate long StatefulSet name prefixes (#10343)
Kubernetes can generate StatefulSet names that are too long and result in invalid Pod revision hash label values.
Calculate whether a StatefulSet name generated for a Service or Ingress
will be too long and if so, truncate it.

Updates tailscale/tailscale#10284

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-21 10:20:37 +00:00
Andrew Lytvynov
2c1f14d9e6 util/set: implement json.Marshaler/Unmarshaler (#10308)
Marshal as a JSON list instead of a map. Because set elements are
`comparable` and not `cmp.Ordered`, we cannot easily sort the items
before marshaling.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-20 08:00:31 -08:00
Irbe Krumina
dd8bc9ba03 cmd/k8s-operator: log user/group impersonated by apiserver proxy (#10334)
Updates tailscale/tailscale#10127

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-20 15:41:18 +00:00
Irbe Krumina
4f80f403be cmd/k8s-operator: fix chart syntax error (#10333)
Updates #9222

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-20 10:39:14 +00:00
License Updater
2fa219440b licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-18 16:39:05 -08:00
David Anderson
f867392970 cmd/tailscale/cli: add debug function to print the netmap
It's possible to do this with a combination of watch-ipn and jq, but looking
at the netmap while debugging is quite common, so it's nice to have a one-shot
command to get it.

Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-11-18 15:04:36 -08:00
David Anderson
fd22145b52 cmd/tailscale/cli: make 'debug watch-ipn' play nice with jq
jq doens't like non-json output in the json stream, and works more happily
when the input stream EOFs at some point. Move non-json words to stderr, and
add a parameter to stop watching and exit after some number of objects.

Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-11-18 14:34:59 -08:00
Ryan Petris
c4855fe0ea Fix Empty Resolver Set
Config.singleResolverSet returns true if all routes have the same resolvers,
even if the routes have no resolvers. If none of the routes have a specific
resolver, the default should be used instead. Therefore, check for more than
0 instead of nil.

Signed-off-by: Ryan Petris <ryan@petris.net>
2023-11-18 14:22:26 -08:00
Uri Gorelik
b88929edf8 Fix potential goroutine leak in syncs/watchdog.go
Depending on how the preemption will occur, in some scenarios sendc
would have blocked indefinitely even after cancelling the context.

Fixes #10315

Signed-off-by: Uri Gorelik <uri.gore@gmail.com>
2023-11-18 10:37:29 -08:00
Flakes Updater
e7cad78b00 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-17 18:55:53 -08:00
OSS Updater
fc8488fac0 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-11-17 18:48:20 -08:00
Will Norris
42dc843a87 client/web: add advanced login options
This adds an expandable section of the login view to allow users to
specify an auth key and an alternate control URL.

Input and Collapsible components and accompanying styles were brought
over from the adminpanel.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-17 18:39:02 -08:00
Flakes Updater
f0613ab606 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-17 15:23:16 -08:00
OSS Updater
3402998c1c go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-11-17 17:34:41 -05:00
Sonia Appasamy
38ea8f8c9c client/web: add Inter font
Adds Inter font and uses it as the default for the web UI.
Creates a new /assets folder to house the /fonts, and moves /icons
to live here too.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-17 17:09:37 -05:00
Marwan Sulaiman
2dc0645368 ipn/ipnlocal,cmd/tailscale: persist tailnet name in user profile
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>
2023-11-17 17:00:11 -05:00
Sonia Appasamy
e75be017e4 client/web: add exit node selector
Add exit node selector (in full management client only) that allows
for advertising as an exit node, or selecting another exit node on
the Tailnet for use.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-17 16:45:33 -05:00
License Updater
0e27ec2cd9 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-17 12:22:59 -08:00
License Updater
4f05cf685e licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-17 12:22:13 -08:00
License Updater
05298f4336 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-17 12:21:49 -08:00
Will Norris
f880c77df0 client/web: split login from nodeUpdate
This creates a new /api/up endpoint which is exposed in the login
client, and is solely focused on logging in. Login has been removed from
the nodeUpdate endpoint.

This also adds support in the LoginClientView for a stopped node that
just needs to reconnect, but not necessarily reauthenticate.  This
follows the same pattern in `tailscale up` of just setting the
WantRunning user pref.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-17 12:12:31 -08:00
James Tucker
28684b0538 cmd/tailscale/cli: correct app connector help text in set
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-17 11:51:36 -08:00
Sonia Appasamy
980f1f28ce client/web: hide unimplemented links
Hiding links to unimplemented settings pages.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-17 14:24:46 -05:00
Brad Fitzpatrick
fb829ea7f1 control/controlclient: support incremental packet filter updates [capver 81]
Updates #10299

Change-Id: I87e4235c668a1db7de7ef1abc743f0beecb86d3d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-17 10:07:12 -08:00
Claire Wang
b8a2aedccd util/syspolicy: add caching handler (#10288)
Fixes tailscale/corp#15850
Co-authored-by: Adrian Dewhurst <adrian@tailscale.com>
Signed-off-by: Claire Wang <claire@tailscale.com>
2023-11-17 12:31:51 -05:00
Ox Cart
719ee4415e ssh/tailssh: use control server time instead of local time
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.

Fixes tailscale/corp#15796

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2023-11-17 11:10:11 -06:00
Sonia Appasamy
bd534b971a {client/web},{ipn/ipnlocal}: replace localapi debug-web-client endpoint
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>
2023-11-16 18:32:52 -05:00
Brad Fitzpatrick
4d196c12d9 health: don't report a warning in DERP homeless mode
Updates #3363
Updates tailscale/corp#396

Change-Id: Ibfb0496821cb58a78399feb88d4206d81e95ca0f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-16 14:08:47 -08:00
Brad Fitzpatrick
cca27ef96a ipn/ipnlocal: add c2n method to check on TLS cert fetch status
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>
2023-11-16 14:08:38 -08:00
Irbe Krumina
664ebb14d9 cmd/containerboot: fix unclean shutdown (#10035)
* cmd/containerboot: shut down cleanly on SIGTERM

Make sure that tailscaled watcher returns when
SIGTERM is received and also that it shuts down
before tailscaled exits.

Updates tailscale/tailscale#10090

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-16 19:23:18 +00:00
Sonia Appasamy
7238586652 client/web: fix margins on login popover
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-16 14:08:08 -05:00
Will Norris
5aa22ff3eb tsnet: add option to run integrated web client
Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-16 11:01:55 -08:00
Tyler Smalley
90eb5379f4 ipn/ipnlocal: log and don't return full file serve error (#10174)
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>
2023-11-16 10:53:40 -08:00
Sonia Appasamy
4f409012c5 client/web: when readonly, add check for TS connection
When the viewing user is accessing a webclient not over Tailscale,
they must connect over Tailscale before being able to log into the
full management client, which is served over TS. This change adds
a check that the user is able to access the node's tailscale IP.
If not able to, the signin button is disabled. We'll also be adding
Copy here to help explain to the user that they must connect to
Tailscale before proceeding.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-16 13:46:54 -05:00
Will Norris
33147c4591 .github: build gocross using regular GOPROXY settings
This `go get` action has been running very slowly, and I'm pretty sure
it's because we're building gocross on the first `./tool/go` run, and
because we've set `GOPROXY=direct`, it's going directly to GitHub to
fetch all of the gocross dependencies.

Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-16 09:46:48 -08:00
Flakes Updater
a3c11b87c6 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-16 09:20:21 -08:00
OSS Updater
146c4bacde go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-11-16 09:14:10 -08:00
Will Norris
d01fa857b1 client/web: allow login client to still run tailscale up
I don't believe this has ever worked, since we didn't allow POST
requests in the login client. But previously, we were primarily using
the legacy client, so it didn't really matter. Now that we've removed
the legacy client, we have no way to login.

This fixes the login client, allowing it to login, but it still needs to
be refactored to expose a dedicated login method, without exposing all
the node update functionality.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-16 07:30:04 -08:00
Brad Fitzpatrick
3bd382f369 wgengine/magicsock: add DERP homeless debug mode for testing
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>
2023-11-15 18:45:10 -08:00
Jordan Whited
2ff54f9d12 wgengine/magicsock: move trustBestAddrUntil forward on non-disco rx (#10274)
This is gated behind the silent disco control knob, which is still in
its infancy. Prior to this change disco pong reception was the only
event that could move trustBestAddrUntil forward, so even though we
weren't heartbeating, we would kick off discovery pings every
trustUDPAddrDuration and mirror to DERP.

Updates #540

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-15 16:30:50 -08:00
Maisem Ali
57129205e6 cmd/tailscaled: make tun mode default on gokrazy
Now that we have nftable support this works fine and force
it on gokrazy since 25a8daf405.

Updates gokrazy/gokrazy#209

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-11-15 15:36:42 -08:00
Sonia Appasamy
96ad9b6138 client/web: remove legacy-client-view.tsx
Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-15 18:12:13 -05:00
Sonia Appasamy
055394f3be ipn/ipnlocal: add mutex to webClient struct
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>
2023-11-15 17:57:48 -05:00
Maisem Ali
7d4221c295 cmd/tsidp: add start of OIDC Tailscale IdP
Updates #10263

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I240bc9b5ecf2df6f92c45929d105fde66c06a860
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-15 14:27:39 -08:00
Sonia Appasamy
2dbd546766 client/web: remove DebugMode from GET /api/data
No longer using this! Readonly state fully managed via auth endpoint.
Also getting rid of old Legacy server mode.

A #cleanup

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-15 17:18:33 -05:00
Sonia Appasamy
6f7a1b51a8 ipn/ipnlocal: rename SetWebLocalClient to ConfigureWebClient
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>
2023-11-15 16:51:28 -05:00
Naman Sood
d5c460e83c client/{tailscale,web}: add initial webUI frontend for self-updates (#10191)
Updates #10187.

Signed-off-by: Naman Sood <mail@nsood.in>
2023-11-15 16:04:44 -05:00
James Tucker
245ddb157b appc: fix DomainRoutes copy
The non-referential copy destination doesn't extend the map contents,
but also the read of a non-key is returning a zero value not bound to
the map contents in any way.

Updates tailscale/corp#15657

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-15 12:20:00 -08:00
Adrian Dewhurst
b8ac3c5191 util/syspolicy: add some additional policy keys
These policy keys are supported on Apple platforms in Swift code; in
order to support them on platforms using Go (e.g. Windows), they also
need to be recorded here.

This does not affect any code, it simply adds the constants for now.

Updates ENG-2240
Updates ENG-2127
Updates ENG-2133

Change-Id: I0aa9863a3641e5844479da3b162761452db1ef42
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-11-15 15:03:21 -05:00
Adrian Dewhurst
1ef5bd5381 util/osdiag, util/winutil: expose Windows policy key
The Windows base registry key is already exported but the policy key was
not. util/osdiag currently replicates the string rather than the
preferred approach of reusing the constant.

Updates #cleanup

Change-Id: I6c1c45337896c744059b85643da2364fb3f232f2
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-11-15 15:01:25 -05:00
Aaron Klotz
855f79fad7 cmd/tailscaled, util/winutil: changes to process and token APIs in winutil
This PR changes the internal getTokenInfo function to use generics.
I also removed our own implementations for obtaining a token's user
and primary group in favour of calling the ones now available in
x/sys/windows.

Furthermore, I added two new functions for working with tokens, logon
session IDs, and Terminal Services / RDP session IDs.

I modified our privilege enabling code to allow enabling of multiple
privileges via one single function call.

Finally, I added the ProcessImageName function and updated the code in
tailscaled_windows.go to use that instead of directly calling the
underlying API.

All of these changes will be utilized by subsequent PRs pertaining to
this issue.

Updates https://github.com/tailscale/corp/issues/13998

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-11-15 12:07:19 -07:00
Will Norris
03e780e9af client/web: disable the "disable" button when disabled
We currently disable the exit-node drop down selector when the user is
in read-only mode, but we missed disabling the "Disable" button also.
Previously, it would display an error when clicked.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-15 11:05:59 -08:00
Will Norris
9b537f7c97 ipn: remove the preview-webclient node capability
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>
2023-11-15 11:04:35 -08:00
Will Norris
303a1e86f5 cmd/tailscale: expose --webclient for all builds
This removes the dev/unstable build check for the --webclient flag on
`tailscale set`, so that it will be included in the next major stable
release (1.56)

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-15 11:02:16 -08:00
Andrew Dunham
e33bc64cff net/dnsfallback: add singleflight to recursive resolver
This prevents running more than one recursive resolution for the same
hostname in parallel, which can use excessive amounts of CPU when called
in a tight loop. Additionally, add tests that hit the network (when
run with a flag) to test the lookup behaviour.

Updates tailscale/corp#15261

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I39351e1d2a8782dd4c52cb04b3bd982eb651c81e
2023-11-15 13:57:49 -05:00
Denton Gentry
a40e918d63 VERSION.txt: this is v1.55.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-11-15 10:15:28 -08:00
James Tucker
e866ee9268 types/appctype: correct app-connector cap name in documentation
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-15 09:47:24 -08:00
Sonia Appasamy
bb31912ea5 cmd/cli: remove --webclient flag from up
Causing issues building a stable release. Getting rid of the flag
for now because it was only available in unstable, can still be
turned on through localapi.

A #cleanup

Co-authored-by: Will Norris <will@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-15 12:47:08 -05:00
Will Norris
1cb8d2ffdd ipn/ipnlocal: only call serve handler if non-nil
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>
2023-11-15 08:28:03 -08:00
Andrea Barisani
05d4210dbe adjust build tags for tamago
Signed-off-by: Andrea Barisani <andrea@inversepath.com>
2023-11-15 06:33:02 -08:00
Will Norris
b7918341f9 ipn/ipnlocal: call serve handler for local traffic
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>
2023-11-14 23:19:43 -08:00
Flakes Updater
e3dacb3e5e go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-14 18:00:19 -08:00
Andrew Lytvynov
c3f1bd4c0a clientupdate: fix auto-update on Windows over RDP (#10242)
`winutil.WTSGetActiveConsoleSessionId` only works for physical desktop
logins and does not return the session ID for RDP logins. We need to
`windows.WTSEnumerateSessions` and find the active session.

Fixes https://github.com/tailscale/corp/issues/15772

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-14 17:21:03 -08:00
Will Norris
60957e1077 client/web: fix back button on devices with URL prefix
Move Header component inside Router so that links are relative to the
router base URL.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-14 15:30:13 -08:00
Will Norris
fb984c2b71 client/web: server /index.html on 404 requests
In production, the asset handler is receiving requests for pages like
/details, which results in a 404. Instead, if we know the requested file
does not exist, serve the main index page and let wouter route it
appropriately on the frontend.

Updates tailscale/corp/#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-14 15:29:55 -08:00
Will Norris
74947ce459 client/web: only trigger check mode if not authed
After logging in, the `?check=now` query string is still present if it
was passed. Reloading the page causes a new check mode to be triggered,
even though the user has an active session. Only trigger the automatic
check mode if the user is not already able to manage the device.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-14 12:27:18 -08:00
Will Norris
79719f05a9 ipn/ipnlocal: remove web client listeners after close
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>
2023-11-14 12:26:14 -08:00
Sonia Appasamy
7c99a1763b client/web: fix panic on logout
Fix panic due to `CurrentTailnet` being nil.

Fixes tailscale/corp#15791

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-14 14:35:13 -05:00
OSS Updater
063657c65f go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2023-11-14 09:47:56 -08:00
Will Norris
7399e56acd .github: add action for updating web-client-prebuilt module
Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-14 09:30:08 -08:00
Andrew Lytvynov
955e2fcbfb ipn/ipnlocal: run "tailscale update" via systemd-run on Linux (#10229)
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>
2023-11-13 16:41:21 -08:00
Jordan Whited
c99488ea19 wgengine/magicsock: fix typo in endpoint.sendDiscoPing() docs (#10232)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-13 13:56:26 -08:00
Tom DNetto
90a0aafdca cmd/tailscale: warn if app-connector is enabled without ip forwarding
Fixes: ENG-2446
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-11-13 13:35:35 -08:00
Brad Fitzpatrick
1825d2337b ipn/ipnlocal: respect ExitNodeAllowLANAccess on iOS (#10230)
Updates tailscale/corp#15783

Change-Id: I1082fbfff61a241ebd3b8275be0f45e329b67561

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-13 13:18:30 -08:00
Sonia Appasamy
c9bfb7c683 client/web: add Tailscale SSH view
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-13 15:49:45 -05:00
Brad Fitzpatrick
103c00a175 ipn/ipnlocal: clean up c2n handling's big switch, add a mux table
Updates #cleanup

Change-Id: I29ec03db91e7831a3a66a63dcf6ff8e3f72ab045
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-13 11:57:56 -08:00
Tom DNetto
ce46d92ed2 go.{mod,sum}: update inet.af/tcpproxy to fix flaking test
```
[nix-shell:~/tailscale]$ ./tool/go test ./cmd/sniproxy --failfast --count 80
ok  	tailscale.com/cmd/sniproxy	127.085s
[nix-shell:~/tailscale]$ ./tool/go test ./cmd/sniproxy --failfast --count 80
ok  	tailscale.com/cmd/sniproxy	127.030s
```

Fixes: #10056
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-11-13 11:48:45 -08:00
Joe Tsai
975c5f7684 taildrop: lazily perform full deletion scan after first taildrop use (#10137)
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>
2023-11-13 12:20:28 -06:00
Jordan Whited
e848736927 control/controlknobs,wgengine/magicsock: implement SilentDisco toggle (#10195)
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>
2023-11-13 10:05:04 -08:00
Kristoffer Dalby
fe7f7bff4f posture: ignore not found serial errors
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-11-13 18:22:08 +01:00
Sonia Appasamy
86c8ab7502 client/web: add readonly/manage toggle
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-10 15:01:34 -05:00
James Tucker
c54d680682 ipn,tailconfig: clean up unreleased and removed app connector service
This was never released, and is replaced by HostInfo.AppConnector.

Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-09 22:36:52 -08:00
James Tucker
0b6636295e tailcfg,ipn/ipnlocal: add hostinfo field to replace service entry
Updates tailscale/corp#15437
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-09 20:50:55 -08:00
Andrew Lytvynov
1f4a38ed49 clientupdate: add support for QNAP (#10179)
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>
2023-11-09 17:46:16 -08:00
James Tucker
45be37cb01 ipn/ipnlocal: ensure that hostinfo is updated on app connector preference changes
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>
2023-11-09 17:38:57 -08:00
James Tucker
933d201bba ipn/policy: mark AppConnector service as interesting
Updates #15437
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-09 17:16:07 -08:00
James Tucker
1a143963ec appc: prevent duplication of wildcard entries on map updates
Updates #15437
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-09 16:47:42 -08:00
Andrew Lytvynov
6cce5fe001 go.toolchain.rev: bump to Go 1.21.4 (#10189)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-09 13:39:56 -08:00
Brad Fitzpatrick
53c4adc982 ssh/tailssh: add envknobs to force override forwarding, sftp, pty
Updates tailscale/corp#15735

Change-Id: Ib1303406be925c3231ce7e0950a173ad12626492
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-09 13:37:54 -08:00
Brad Fitzpatrick
ffabe5fe21 ssh/tailssh: fix sftp metric increment location
We were incrementing the sftp metric on regular sessions
too, not just sftp.

Updates #cleanup

Change-Id: I63027a39cffb3e03397c6e4829b1620c10fa3130
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-09 13:08:07 -08:00
Naman Sood
e57fd9cda4 ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update (#10188)
* ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update

Updates #10187.

Signed-off-by: Naman Sood <mail@nsood.in>

* depaware

Updates #10187.

Signed-off-by: Naman Sood <mail@nsood.in>

* address review feedback

Signed-off-by: Naman Sood <mail@nsood.in>

---------

Signed-off-by: Naman Sood <mail@nsood.in>
2023-11-09 16:00:47 -05:00
Andrew Lytvynov
55cd5c575b ipn/localapi: only perform local-admin check in serveServeConfig (#10163)
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>
2023-11-09 12:55:46 -08:00
James Tucker
73de6a1a95 appc: add support for matching wildcard domains
The app connector matches a configuration of "*.example.com" to mean any
sub-domain of example.com.

Updates #15437

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-09 12:39:30 -08:00
Jordan Whited
12d5c99b04 client/tailscale,ipn/{ipnlocal,localapi}: check UDP GRO config (#10071)
Updates tailscale/corp#9990

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-11-09 11:34:41 -08:00
Andrew Lytvynov
1fc1077052 ssh/tailssh,util: extract new osuser package from ssh code (#10170)
This package is a wrapper for os/user that handles non-cgo builds,
gokrazy and user shells.

Updates tailscale/corp#15405

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-09 09:49:06 -08:00
Will Norris
09de240934 ipn/ipnlocal: allow connecting to local web client
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>
2023-11-09 08:49:15 -08:00
Brad Fitzpatrick
d36a0d42aa tsnet: check a bit harder for https in Server.ListenFunnel
This was mostly already fixed already indirectly in earlier
commits but add a last second length check to this slice so
it can't ever OOB.

Fixes #7860

Change-Id: I31ac17fc93b5808deb09ff34e452fe37c87ddf3a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-09 07:51:58 -08:00
Andrew Lytvynov
bff786520e clientupdate,ipn/ipnlocal: fix c2n update on freebsd (#10168)
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>
2023-11-08 18:56:00 -07:00
Sonia Appasamy
d544e80fc1 client/web: populate device details view
Fills /details page with real values, passed back from the /data
endpoint.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-08 17:59:58 -05:00
Brad Fitzpatrick
d852c616c6 logtail: fix Logger.Write return result
io.Writer says you need to write completely on err=nil. (the result
int should be the same as the input buffer length)

We weren't doing that. We used to, but at some point the verbose
filtering was modifying buf before the final return of len(buf).

We've been getting lucky probably, that callers haven't looked at our
results and turned us into a short write error.

Updates #cleanup
Updates tailscale/corp#15664

Change-Id: I01e765ba35b86b759819e38e0072eceb9d10d75c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-08 11:57:15 -08:00
Tom DNetto
11a20f371a ipn/ipnlocal: fix nil control client panic while updating TKA head
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>
2023-11-08 10:44:25 -08:00
Tom DNetto
3496d62ed3 ipn/ipnlocal: add empty address to the app-connector localNets set
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
2023-11-08 10:44:03 -08:00
Will Norris
fdbe511c41 cmd/tailscale: add -webclient flag to up and set
Initially, only expose this flag on dev and unstable builds.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-08 10:00:58 -08:00
Charlotte Brandhorst-Satzkorn
f937cb6794 tailcfg,ipn,appc: add c2n endpoint for appc domain routes
This change introduces a c2n endpoint that returns a map of domains to a
slice of resolved IP addresses for the domain.

Fixes tailscale/corp#15657

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-11-07 18:12:24 -08:00
Andrew Lytvynov
63062abadc clientupdate: check whether running as root early (#10161)
Check for root early, before we fetch the pkgs index. This avoids
several seconds delay for the command to tell you to sudo.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-07 13:09:30 -08:00
Andrew Lytvynov
9b158db2c6 ipn/localapi: require root or sudo+operator access for SetServeConfig (#10142)
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>
2023-11-07 12:31:33 -08:00
Will Norris
fc2d63bb8c go.mod: updates web-client-prebuilt
Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-07 11:16:37 -08:00
Will Norris
623f669239 client/web: pass URL prefix to frontend
This allows wouter to route URLs properly when running in CGI mode.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-07 11:13:01 -08:00
Sonia Appasamy
0753ad6cf8 client/web: move useNodeData out of App component
Only loading data once auth request has completed.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-07 13:31:05 -05:00
Will Norris
d530153d2f go.mod: bump web-client-prebuilt
Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-07 09:29:00 -08:00
Sonia Appasamy
5e095ddc20 client/web: add initial framework for exit node selector
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-07 11:30:22 -05:00
Sonia Appasamy
de2af54ffc client/web: pipe newSession through to readonly view
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-07 11:12:40 -05:00
Sonia Appasamy
d73e923b73 client/web: add device details view
Initial addition of device details view on the frontend. A little
more backend piping work to come to fill all of the detail fields,
for now using placeholders.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-07 10:59:18 -05:00
Will Norris
3e9026efda client/web: show manage button in readonly view
We render the readonly view in two situations:
- the client is in login mode, and the device is connected
- the client is in manage mode, but the user does not yet have a session

If the user is not authenticated, and they are not currently on the
Tailscale IP address, render a "Manage" button that will take them to
the Tailcale IP of the device and immediately start check mode.

Still to do is detecting if they have connectivity to the Tailscale IP,
and disabling the button if not.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-07 07:50:26 -08:00
Thomas Kosiewski
96a80fcce3 Add support for custom DERP port in TLS prober
Updates #10146

Signed-off-by: Thomas Kosiewski <thoma471@googlemail.com>
2023-11-07 12:56:13 +00:00
Charlotte Brandhorst-Satzkorn
839fee9ef4 wgengine/magicsock: handle wireguard only clean up and log messages
This change updates log messaging when cleaning up wireguard only peers.
This change also stops us unnecessarily attempting to clean up disco
pings for wireguard only endpoints.

Updates #7826

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-11-06 16:26:31 -08:00
Sonia Appasamy
3269b36bd0 client/web: fix hotreload proxy
Previously had HMR websocket set to run from a different port
than the http proxy server. This was an old setting carried over
from the corp repo admin panel config. It's messing with hot
reloads when run from the tailscaled web client, as it keeps
causing the full page to refresh each time a connection is made.
Switching back to the default config here fixes things.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-06 16:31:30 -05:00
Sonia Appasamy
942d720a16 cli/web: don't block startup on status req
If the status request to check for the preview node cap fails,
continue with starting up the legacy client.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-06 16:19:16 -05:00
Sonia Appasamy
7df2c5d6b1 client/web: add route management for ui pages
Using wouter, a lightweight React routing library.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-06 15:55:55 -05:00
License Updater
a97ead9ce4 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-06 12:37:18 -08:00
License Updater
aeb5a8b123 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-11-06 12:36:44 -08:00
Sonia Appasamy
f2a4c4fa55 client/web: build out client home page
Hooks up more of the home page UI.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-06 15:35:15 -05:00
Andrew Lytvynov
aba4bd0c62 util/winutil: simplify dropping privileges after use (#10099)
To safely request and drop privileges, runtime.Lock/UnlockOSThread and
windows.Impersonate/RevertToSelf should be called. Add these calls to
winutil.EnableCurrentThreadPrivilege so that callers don't need to worry
about it.

Updates https://github.com/tailscale/corp/issues/15488

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-06 11:37:37 -08:00
Anton Tolchanov
ef6a6e94f1 derp/derphttp: use a getter method to read server key
To hold the mutex while accessing it.

Fixes #10122

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-11-06 14:58:55 +00:00
Brad Fitzpatrick
44c6909c92 control/controlclient: move watchdog out of mapSession
In prep for making mapSession's lifetime not be 1:1 with a single HTTP
response's lifetime, this moves the inactivity timer watchdog out of
mapSession and into the caller that owns the streaming HTTP response.

(This is admittedly closer to how it was prior to the mapSession type
existing, but that was before we connected some dots which were
impossible to even see before the mapSession type broke the code up.)

Updates #7175

Change-Id: Ia108dac84a4953db41cbd30e73b1de4a2a676c11
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-05 10:57:36 -08:00
Brad Fitzpatrick
c87d58063a control/controlclient: move lastPrintMap field from Direct to mapSession
It was a really a mutable field owned by mapSession that we didn't move
in earlier commits.

Once moved, it's then possible to de-func-ify the code and turn it into
a regular method rather than an installed optional hook.

Noticed while working to move map session lifetimes out of
Direct.sendMapRequest's single-HTTP-connection scope.

Updates #7175
Updates #cleanup

Change-Id: I6446b15793953d88d1cabf94b5943bb3ccac3ad9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-05 08:53:47 -08:00
Matt Layher
1a1e0f460a client/tailscale: remove redundant error check
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2023-11-05 07:40:23 -08:00
Will Norris
e537d304ef client/web: relax CSP restrictions for manage client
Don't return CSP headers in dev mode, since that includes a bunch of
extra things like the vite server.

Allow images from any source, which is needed to load user profile
images.

Allow 'unsafe-inline' for various inline scripts and style react uses.
We can eliminate this by using CSP nonce or hash values, but we'll need
to look into the best way to handle that. There appear to be several
react plugins for this, but I haven't evaluated any of them.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-05 01:11:21 -07:00
Claire Wang
5de8650466 syspolicy: add Allow LAN Access visibility key (#10113)
Fixes tailscale/corp#15594

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-11-04 15:51:20 -04:00
Brad Fitzpatrick
b2b836214c derp/derphttp: fix derptrack fix
3d7fb6c21d dropped the explicit called to (*Client).connect when
its (*Client).WatchConnectionChanges got removed+refactored.

This puts it back, but in RunWatchConnectionLoop, before the call
to the (*Client).ServerPublicKey accessor, which is documented to
return the zero value (which is what broke us) on an unconnected
connection.

Plus some tests.

Fixes tailscale/corp#15604

Change-Id: I0f242816f5ee4ad3bb0bf0400abc961dbe9f5fc8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-04 11:48:40 -07:00
Flakes Updater
8dc6de6f58 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-11-03 19:28:46 -07:00
Will Norris
7e81c83e64 cmd/tailscale: respect existing web client pref
After running `tailscale web`, only disable the user pref if it was not
already previously set.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-03 17:26:52 -07:00
Will Norris
cb07ed54c6 go.mod: update web-client-prebuilt
Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-03 15:00:41 -07:00
Will Norris
a05ab9f3bc client/web: check r.Host rather than r.URL.Host
r.URL.Host is not typically populated on server requests.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-03 14:55:16 -07:00
Will Norris
6b956b49e0 client/web: add some security checks for full client
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>
2023-11-03 14:15:59 -07:00
Aaron Klotz
fbc18410ad ipn/ipnauth: improve the Windows token administrator check
(*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>
2023-11-03 14:37:04 -06:00
Sonia Appasamy
e5dcf7bdde client/web: move auth session creation out of /api/auth
Splits auth session creation into two new endpoints:

/api/auth/session/new - to request a new auth session

/api/auth/session/wait - to block until user has completed auth url

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-03 15:30:04 -04:00
Will Norris
658971d7c0 ipn/ipnlocal: serve web client on quad100 if enabled
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>
2023-11-03 12:29:10 -07:00
James Tucker
46fd488a6d types/dnstype: update the usage documentation on dnstype.Resolver
There was pre-existing additional usage for Exit Node DNS resolution via
PeerAPI, as well as new usage just introduced for App Connectors.

Fixes ENG-2324
Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-03 09:01:11 -07:00
Sonia Appasamy
0ecfc1d5c3 client/web: fill devMode from an env var
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>
2023-11-02 21:51:33 -04:00
Andrew Lytvynov
f0bc95a066 ipn/localapi: make serveTKASign require write permission (#10094)
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>
2023-11-02 17:01:26 -06:00
Sonia Appasamy
191e2ce719 client/web: add ServerMode to web.Server
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>
2023-11-02 17:20:05 -04:00
Andrew Lytvynov
7145016414 clientupdate: do not recursively delete dirs in cleanupOldDownloads (#10093)
In case there's a wild symlink in one of the target paths, we don't want
to accidentally delete too much. Limit `cleanupOldDownloads` to deleting
individual files only.

Updates https://github.com/tailscale/tailscale/issues/10082

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-02 13:29:52 -07:00
Will Norris
4ce4bb6271 client/web: limit authorization checks to API calls
This completes the migration to setting up authentication state in the
client first before fetching any node data or rendering the client view.

Notable changes:
 - `authorizeRequest` is now only enforced on `/api/*` calls (with the
   exception of /api/auth, which is handled early because it's needed to
   initially setup auth, particularly for synology)
 - re-separate the App and WebClient components to ensure that auth is
   completed before moving on
 - refactor platform auth (synology and QNAP) to fit into this new
   structure. Synology no longer returns redirect for auth, but returns
   authResponse instructing the client to fetch a SynoToken

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-02 13:01:09 -07:00
James Tucker
f27b2cf569 appc,cmd/sniproxy,ipn/ipnlocal: split sniproxy configuration code out of appc
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>
2023-11-02 12:51:40 -07:00
Andrew Lytvynov
6c0ac8bef3 clientupdate: cleanup SPK and MSI downloads (#10085)
After we're done installing, clean up the temp files. This prevents temp
volumes from filling up on hosts that don't reboot often.

Fixes https://github.com/tailscale/tailscale/issues/10082

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-02 12:21:42 -06:00
Sonia Appasamy
aa5af06165 ipn/ipnlocal: include web client port in setTCPPortsIntercepted
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-02 13:56:26 -04:00
Sonia Appasamy
da31ce3a64 ipn/localapi: remove webclient endpoint
Managing starting/stopping tailscaled web client purely via setting
the RunWebClient pref.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-02 13:42:55 -04:00
Sonia Appasamy
b370274b29 ipn/ipnlocal: pull CapabilityPreviewWebClient into webClientAtomicBool
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>
2023-11-02 13:22:50 -04:00
Andrew Lytvynov
c6a4612915 ipn/localapi: require Write access on /watch-ipn-bus with private keys (#10059)
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>
2023-11-02 09:48:10 -07:00
Aaron Klotz
47019ce1f1 cmd/tailscaled: pre-load wintun.dll using a fully-qualified path
In corp PR #14970 I updated the installer to set a security mitigation that
always forces system32 to the front of the Windows dynamic linker's search
path.

Unfortunately there are other products out there that, partying like it's
1995, drop their own, older version of wintun.dll into system32. Since we
look there first, we end up loading that old version.

We can fix this by preloading wintun using a fully-qualified path. When
wintun-go then loads wintun, the dynamic linker will hand it the module
that was previously loaded by us.

Fixes #10023, #10025, #10052

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-11-02 09:47:21 -06:00
Irbe Krumina
af49bcaa52 cmd/k8s-operator: set different app type for operator with proxy (#10081)
Updates tailscale/tailscale#9222

plain k8s-operator should have hostinfo.App set to 'k8s-operator', operator with proxy should have it set to 'k8s-operator-proxy'. In proxy mode, we were setting the type after it had already been set to 'k8s-operator'

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-02 14:36:20 +00:00
Brad Fitzpatrick
673ff2cb0b util/groupmember: fail earlier if group doesn't exist, use slices.Contains
Noticed both while re-reading this code.

Updates #cleanup

Change-Id: I3b70f1d5dc372853fa292ae1adbdee8cfc6a9a7b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-11-01 19:23:16 -07:00
James Tucker
228a82f178 ipn/ipnlocal,tailcfg: add AppConnector service to HostInfo when configured
Updates tailscale/corp#15437

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 16:37:24 -07:00
James Tucker
6ad54fed00 appc,ipn/ipnlocal: add App Connector domain configuration from mapcap
The AppConnector is now configured by the mapcap from the control plane.

Updates tailscale/corp#15437

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 16:37:09 -07:00
James Tucker
e9de59a315 tstest/deptest: fix minor escaping error in regex
Fixes https://github.com/tailscale/tailscale/security/code-scanning/112

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 16:22:18 -07:00
James Tucker
b48b7d82d0 appc,ipn/ipnlocal,net/dns/resolver: add App Connector wiring when enabled in prefs
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>
2023-11-01 16:09:08 -07:00
Will Norris
e7482f0df0 ipn/ipnlocal: prevent deadlock on WebClientShutdown
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>
2023-11-01 15:36:05 -07:00
Sonia Appasamy
7a725bb4f0 client/web: move more session logic to auth.go
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-01 18:35:43 -04:00
dependabot[bot]
535cb6c3f5 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.6+incompatible to 24.0.7+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.6...v24.0.7)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 15:28:49 -07:00
dependabot[bot]
f2bc54ba15 build(deps-dev): bump postcss from 8.4.27 to 8.4.31 in /client/web
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 15:25:57 -07:00
dependabot[bot]
6cc81a6d3e build(deps): bump get-func-name from 2.0.0 to 2.0.2 in /client/web
Bumps [get-func-name](https://github.com/chaijs/get-func-name) from 2.0.0 to 2.0.2.
- [Release notes](https://github.com/chaijs/get-func-name/releases)
- [Commits](https://github.com/chaijs/get-func-name/commits/v2.0.2)

---
updated-dependencies:
- dependency-name: get-func-name
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 15:25:29 -07:00
dependabot[bot]
80fc32588c build(deps): bump @babel/traverse from 7.22.10 to 7.23.2 in /client/web
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 15:24:09 -07:00
Will Norris
e5fbe57908 web/client: update synology token from /api/auth call
When the /api/auth response indicates that synology auth is needed,
fetch the SynoToken and store it for future API calls.  This doesn't yet
update the server-side code to set the new SynoAuth field.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-01 14:18:40 -07:00
dependabot[bot]
b1a0caf056 .github: Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 13:26:33 -07:00
Andrew Lytvynov
7f16e000c9 clientupdate: clarify how to run update as Administrator on Windows (#10043)
Make the error message a bit more helpful for users.

Updates #9456

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-11-01 13:15:17 -07:00
James Tucker
01604c06d2 hostinfo: fix a couple of logic simplification lints
Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 13:14:25 -07:00
David Anderson
37863205ec cmd/k8s-operator: strip credentials from client config in noauth mode
Updates tailscale/corp#15526

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-11-01 13:13:40 -07:00
James Tucker
0ee4573a41 ipn/ipnlocal: fix small typo
Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 12:42:11 -07:00
Will Norris
237c6c44cd client/web: call /api/auth before rendering any client views
For now this is effectively a noop, since only the ManagementClientView
uses the auth data. That will change soon.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-11-01 12:09:38 -07:00
Rhea Ghosh
970eb5e784 cmd/k8s-operator: sanitize connection headers (#10063)
Fixes tailscale/corp#15526

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2023-11-01 13:15:57 -05:00
James Tucker
ca4c940a4d ipn: introduce app connector advertisement preference and flags
Introduce a preference structure to store the setting for app connector
advertisement.

Introduce the associated flags:

  tailscale up --advertise-connector{=true,=false}
  tailscale set --advertise-connector{=true,=false}

```
% tailscale set --advertise-connector=false
% tailscale debug prefs | jq .AppConnector.Advertise
false
% tailscale set --advertise-connector=true
% tailscale debug prefs | jq .AppConnector.Advertise
true
% tailscale up --advertise-connector=false
% tailscale debug prefs | jq .AppConnector.Advertise
false
% tailscale up --advertise-connector=true
% tailscale debug prefs | jq .AppConnector.Advertise
true
```

Updates tailscale/corp#15437

Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 10:58:54 -07:00
James Tucker
09fcbae900 net/dnscache: remove completed TODO
The other IP types don't appear to be imported anymore, and after a scan
through I couldn't see any substantial usage of other representations,
so I think this TODO is complete.

Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-11-01 10:55:47 -07:00
Sonia Appasamy
32ebc03591 client/web: move session logic to auth.go
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-11-01 13:29:59 -04:00
Chris Palmer
3a9f5c02bf util/set: make Clone a method (#10044)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-11-01 10:20:38 -07:00
Derek Kaser
5289cfce33 clientupdate: disable on Unraid (#10048)
* clientupdate: disable on Unraid

Updates dkaser/unraid-tailscale#94

Signed-off-by: Derek Kaser <derek.kaser@gmail.com>

* Update clientupdate/clientupdate.go

Signed-off-by: Andrew Lytvynov <andrew@awly.dev>

---------

Signed-off-by: Derek Kaser <derek.kaser@gmail.com>
Signed-off-by: Andrew Lytvynov <andrew@awly.dev>
Co-authored-by: Andrew Lytvynov <andrew@awly.dev>
2023-11-01 10:19:22 -07:00
Irbe Krumina
c2b87fcb46 cmd/k8s-operator/deploy/chart,.github/workflows: use helm chart API v2 (#10055)
API v1 is compatible with helm v2 and v2 is not.
However, helm v2 (the Tiller deployment mechanism) was deprecated in 2020
and no-one should be using it anymore.
This PR also adds a CI lint test for helm chart

Updates tailscale/tailscale#9222

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-11-01 16:15:18 +00:00
Maisem Ali
d0f2c0664b wgengine/netstack: standardize var names in UpdateNetstackIPs
Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-11-01 08:29:32 -07:00
Maisem Ali
eaf8aa63fc wgengine/netstack: remove unnecessary map in UpdateNetstackIPs
Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-11-01 08:29:32 -07:00
Maisem Ali
d601c81c51 wgengine/netstack: use netip.Prefix as map keys
Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-11-01 08:29:32 -07:00
Anton Tolchanov
c3313133b9 derp/derphttp: close DERP client to avoid data race in test
Fixes #10041

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-11-01 07:07:42 -07:00
Will Norris
66c7af3dd3 ipn: replace web client debug flag with node capability
Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-10-31 20:51:24 -07:00
Jordan Whited
bd488e4ff8 go.mod: update wireguard-go (#10046)
Updates tailscale/corp#9990

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-10-31 19:56:03 -07:00
Chris Palmer
00375f56ea util/set: add some more Set operations (#10022)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-10-31 17:15:40 -07:00
Andrew Lytvynov
7f3208592f clientupdate: mention release track when running latest (#10039)
Not all users know about our tracks and versioning scheme. They can be
confused when e.g. 1.52.0 is out but 1.53.0 is available. Or when 1.52.0
is our but 1.53 has not been built yet and user is on 1.51.x.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-31 15:50:55 -07:00
Sonia Appasamy
44175653dc ipn/ipnlocal: rename web fields/structs to webClient
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>
2023-10-31 15:40:06 -04:00
Anton Tolchanov
3114a1c88d derp/derphttp: add watch reconnection tests from #9719
Co-authored-by: Val <valerie@tailscale.com>
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-10-31 19:24:25 +00:00
Brad Fitzpatrick
3d7fb6c21d derp/derphttp: fix race in mesh watcher
The derphttp client automatically reconnects upon failure.

RunWatchConnectionLoop called derphttp.Client.WatchConnectionChanges
once, but that wrapper method called the underlying
derp.Client.WatchConnectionChanges exactly once on derphttp.Client's
currently active connection. If there's a failure, we need to re-subscribe
upon all reconnections.

This removes the derphttp.Client.WatchConnectionChanges method, which
was basically impossible to use correctly, and changes it to be a
boolean field on derphttp.Client alongside MeshKey and IsProber. Then
it moves the call to the underlying derp.Client.WatchConnectionChanges
to derphttp's client connection code, so it's resubscribed on any
reconnect.

Some paranoia is then added to make sure people hold the API right,
not calling derphttp.Client.RunWatchConnectionLoop on an
already-started Client without having set the bool to true. (But still
auto-setting it to true if that's the first method that's been called
on that derphttp.Client, as is commonly the case, and prevents
existing code from breaking)

Fixes tailscale/corp#9916
Supercedes tailscale/tailscale#9719

Co-authored-by: Val <valerie@tailscale.com>
Co-authored-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <anton@tailscale.com>
Signed-off-by: Brad Fitzpatrick <brad@danga.com>
2023-10-31 19:24:25 +00:00
Tom DNetto
df4b730438 types/appctype: define the nodeAttrs type for dns-driven app connectors
Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15440
Code-authored-by: Podtato <podtato@tailscale.com>
2023-10-31 12:34:09 -06:00
Tom DNetto
a7c80c332a cmd/sniproxy: implement support for control configuration, multiple addresses
* Implement missing tests for sniproxy
 * Wire sniproxy to new appc package
 * Add support to tsnet for routing subnet router traffic into netstack, so it can be handled

Updates: https://github.com/tailscale/corp/issues/15038
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-10-31 12:19:17 -06:00
Flakes Updater
0d86eb9da5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-31 10:36:16 -07:00
Will Norris
ea599b018c ipn: serve web client requests from LocalBackend
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>
2023-10-31 10:34:56 -07:00
Will Norris
28ad910840 ipn: add user pref for running web client
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>
2023-10-31 10:34:56 -07:00
Jordan Whited
dd842d4d37 go.mod: update wireguard-go to enable TUN UDP GSO/GRO (#10029)
Updates tailscale/corp#9990

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-10-31 10:23:52 -07:00
Sonia Appasamy
6f214dec48 client/web: split out UI components
This commit makes the following structural changes to the web
client interface. No user-visible changes.

1. Splits login, legacy, readonly, and full management clients into
   their own components, and pulls them out into their own view files.
2. Renders the same Login component for all scenarios when the client
   is not logged in, regardless of legacy or debug mode. Styling comes
   from the existing legacy login, which is removed from legacy.tsx
   now that it is shared.
3. Adds a ui folder to hold non-Tailscale-specific components,
   starting with ProfilePic, previously housed in app.tsx.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-31 13:15:07 -04:00
Sonia Appasamy
89953b015b ipn/ipnlocal,client/web: add web client to tailscaled
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>
2023-10-31 13:15:07 -04:00
Sonia Appasamy
93aa8a8cff client/web: allow providing logger implementation
Also report metrics in separate go routine with a 5 second timeout.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-31 13:15:07 -04:00
Andrea Gottardo
95715c4a12 ipn/localapi: add endpoint to handle APNS payloads (#9972)
* 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>
2023-10-30 18:35:53 -07:00
Andrew Dunham
57c5b5a77e net/dns/recursive: update IP for b.root-servers.net
As of 2023-11-27, the official IP addresses for b.root-servers.net will
change to a new set, with the older IP addresses supported for at least
a year after that date. These IPs are already active and returning
results, so update these in our recursive DNS resolver package so as to
be ready for the switchover.

See: https://b.root-servers.org/news/2023/05/16/new-addresses.html

Fixes #9994

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I29e2fe9f019163c9ec0e62bdb286e124aa90a487
2023-10-30 15:39:55 -04:00
Tom DNetto
3df305b764 tsnet: enable use-cases with non-native IPs by setting ns.ProcessSubnets
Terminating traffic to IPs which are not the native IPs of the node requires
the netstack subsystem to intercept trafic to an IP it does not consider local.
This PR switches on such interception. In addition to supporting such termination,
this change will also enable exit nodes and subnet routers when running in
userspace mode.

DO NOT MERGE until 1.52 is cut.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15038
2023-10-30 12:25:27 -06:00
Irbe Krumina
452f900589 tool: download helm CLI (#9981)
Updates tailscale/tailscale#9222

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-10-30 18:20:33 +00:00
Irbe Krumina
ed1b935238 cmd/k8s-operator: allow to install operator via helm (#9920)
Initial helm manifests.

Updates tailscale/tailscale#9222

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Maisem Ali <maisem@tailscale.com>
2023-10-30 18:18:09 +00:00
Tyler Smalley
fde2ba5bb3 VERSION.txt: this is v1.53.0 (#10018)
Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-30 10:45:14 -07:00
Maisem Ali
62d580f0e8 util/linuxfw: add missing error checks in tests
This would surface as panics when run on Fly. Still fail, but at least don't panic.

Updates #10003

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-28 09:44:53 -07:00
Rhea Ghosh
387a98fe28 ipn/ipnlocal: exclude tvOS devices from taildrop file targets (#10002) 2023-10-27 16:35:18 -05:00
Chris Palmer
f66dc8dc0a clientupdate: check for privileges earlier (#9964)
Fixes #9963

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-10-27 10:43:50 -07:00
Flakes Updater
f9fafe269a go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-27 10:37:26 -07:00
Cole Helbling
087260734b flake: drop unnecessary git input
Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-10-27 10:33:50 -07:00
Cole Helbling
561e7b61c3 flake: fix systemd service by hardcoding $PORT
Another solution would be to copy the `.defaults` file alongside the
service file, and set the `EnvironmentFile` to point to that, but it
would still be hardcoded (as the `.defaults` file would be stored in the
Nix store), so I figured that this is a good solution until there is a
proper NixOS module.

Fixes #9995.

Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-10-27 10:33:50 -07:00
Cole Helbling
9e71851a36 go.mod.sri: update
Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-10-27 10:33:50 -07:00
Cole Helbling
4f62a2ed99 flake: set a default package
This allows `nix build` to run, without needing to use `nix build
.#tailscale`.

Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-10-27 10:33:50 -07:00
Cole Helbling
f737496d7c flake: drop unnecessary fileContents binding
Since the tailscale derivation already has a `pkgs` binding, we can
use `pkgs.lib`. Alternatively, we could have used `nixpkgs.lib`, as
`fileContents` doesn't need a system to use (anymore?).

Signed-off-by: Cole Helbling <cole.helbling@determinate.systems>
2023-10-27 10:33:50 -07:00
Maisem Ali
9107b5eadf cmd/tailscale/cli: use status before doing interactive feature query
We were inconsistent whether we checked if the feature was already
enabled which we could do cheaply using the locally available status.
We would do the checks fine if we were turning on funnel, but not serve.

This moves the cap checks down into enableFeatureInteractive so that
are always run.

Updates #9984

Co-authored-by: Tyler Smalley <tyler@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-26 17:10:24 -07:00
License Updater
e94d345e26 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-10-26 15:21:33 -07:00
License Updater
7c7f60be22 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-10-26 15:20:25 -07:00
Tyler Smalley
baa1fd976e ipn/localapi: require local Windows admin to set serve path (#9969)
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>
2023-10-26 14:40:44 -07:00
Will Norris
42abf13843 .github: run tests on all PRs, regardless of branch name
The branch name selector "*" doesn't match branches with a "/" in their
name. The vast majority of our PRs are against the main (or previously,
master) branch anyway, so this will have minimal impact. But in the rare
cases that we want to open a PR against a branch with a "/" in the name,
tests should still run.

```
gh pr list --limit 9999 --state all --json baseRefName | \
  jq -cs '.[] | group_by(.baseRefName) |
    map({ base: .[0].baseRefName, count: map(.baseRefName) | length}) |
    sort_by(-.count) | .[]'

{"base":"main","count":4593}
{"base":"master","count":226}
{"base":"release-branch/1.48","count":4}
{"base":"josh-and-adrian-io_uring","count":3}
{"base":"release-branch/1.30","count":3}
{"base":"release-branch/1.32","count":3}
{"base":"release-branch/1.20","count":2}
{"base":"release-branch/1.26","count":2}
{"base":"release-branch/1.34","count":2}
{"base":"release-branch/1.38","count":2}
{"base":"Aadi/speedtest-tailscaled","count":1}
{"base":"josh/io_uring","count":1}
{"base":"maisem/hi","count":1}
{"base":"rel-144","count":1}
{"base":"release-branch/1.18","count":1}
{"base":"release-branch/1.2","count":1}
{"base":"release-branch/1.22","count":1}
{"base":"release-branch/1.24","count":1}
{"base":"release-branch/1.4","count":1}
{"base":"release-branch/1.46","count":1}
{"base":"release-branch/1.8","count":1}
{"base":"web-client-main","count":1}
```

Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2023-10-26 10:47:39 -07:00
Brad Fitzpatrick
b4be4f089f safesocket: make clear which net.Conns are winio types
Follow-up to earlier #9049.

Updates #9049

Change-Id: I121fbd2468770233a23ab5ee3df42698ca1dabc2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-26 10:11:08 -07:00
Aaron Klotz
95671b71a6 ipn, safesocket: use Windows token in LocalAPI
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>
2023-10-26 09:43:19 -06:00
Andrew Dunham
ef596aed9b net/portmapper: avoid alloc in getUPnPErrorsMetric
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iea558024c038face24cc46584421998d10f13a66
2023-10-26 03:02:27 +02:00
James Tucker
237b4b5a2a .github/workflows: add checklocks
Currently the checklocks step is not configured to fail, as we do not
yet have the appropriate annotations.

Updates tailscale/corp#14381

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-25 16:39:31 -07:00
Tyler Smalley
131518eed1 cmd/tailscale/cli: improve error when bg serve config is present (#9961)
We prevent shodow configs when starting a foreground when a background serve config already exists for the serve type and port. This PR improves the messaging to let the user know how to remove the previous config.

Updates #8489
ENG-2314

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-25 14:27:46 -07:00
Tyler Smalley
1873bc471b cmd/tailscale/cli: remove http flag for funnel command (#9955)
The `--http` flag can not be used with Funnel, so we should remove it to remove confusion.

Updates #8489
ENG-2316

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-25 14:05:54 -07:00
Val
19e5f242e0 net/portmapper: convert UPnP metrics to new syncs.Map.LoadOrInit method
Simplify UPnP error metrics by using the new syncs.Map.LoadOrInit method.

Updates #cleanup

Signed-off-by: Val <valerie@tailscale.com>
2023-10-25 22:39:47 +02:00
Andrew Lytvynov
8326fdd60f clientupdate: disable auto-updates on Synology for now (#9965)
Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-25 16:02:46 -04:00
Flakes Updater
143bda87a3 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-24 20:16:27 -07:00
Marwan Sulaiman
5f3cdaf283 cmd/tailscale/cli: chage port flags to uint for serve and funnel
This PR changes the -https, -http, -tcp, and -tls-terminated-tcp
flags from string to int and also updates the validation to ensure
they fit the uint16 size as the flag library does not have a Uint16Var
method.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-24 20:12:17 -04:00
Andrea Gottardo
741d7bcefe Revert "ipn/ipnlocal: add new DNS and subnet router policies" (#9962)
This reverts commit 32194cdc70.

Signed-off-by: Nick O'Neill <nick@tailscale.com>
2023-10-24 17:07:25 -07:00
Marwan Sulaiman
a7e4cebb90 cmd/tailscale/cli: refactor TestServeDevConfigMutations
The TestServeDevConfigMutations test has 63 steps that all run
under the same scope. This tests breaks them out into isolated
subtests that can be run independently.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-24 18:07:24 -04:00
Sonia Appasamy
d79e0fde9c client/web: split errTaggedSelf resp from getTailscaleBrowserSession
Previously returned errTaggedSource in the case that of any tagged
source. Now distinguishing whether the source was local or remote.
We'll be presenting the two cases with varying copy on the frontend.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-24 16:12:07 -04:00
Sonia Appasamy
e0a4a02b35 client/web: pipe Server.timeNow() through session funcs
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-24 15:46:13 -04:00
Andrew Lytvynov
21b6d373b0 cmd/tailscale/cli: unhide auto-update flags and mark update as Beta (#9954)
Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-24 11:34:24 -07:00
Adrian Dewhurst
32194cdc70 ipn/ipnlocal: add new DNS and subnet router policies
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>
2023-10-24 14:33:59 -04:00
Marwan Sulaiman
f5a7551382 cmd/tailscale: fix help message for serve funnel
We currently print out "run tailscale serve --help" when the subcmd
might be funnel. This PR ensures the right subcmd is passed.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-24 14:32:11 -04:00
Andrew Lytvynov
d3bc575f35 cmd/tailscale/cli: set Sparkle auto-update on macsys (#9952)
On `tailscale set --auto-update`, set the Sparkle plist option for it.
Also make macsys report not supporting auto-updates over c2n, since they
will be triggered by Sparkle locally.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-24 12:17:55 -06:00
James Tucker
6f69fe8ad7 wgnengine: remove unused field in userspaceEngine
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-24 11:01:19 -07:00
Tyler Smalley
269a498c1e cmd/tailscale/cli: serve --set-path should not force background mode (#9905)
A few people have run into issues with understanding why `--set-path` started in background mode, and/or why they couldn't use a path in foreground mode. This change allows `--set-path` to be used in either case (foreground or background).

updates #8489

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-24 09:56:41 -07:00
Thomas Kosiewski
b2ae8fdf80 derp/derphttp: strip port numbers from URL hostname
When trying to set up multiple derper instances meshing with each
other, it turned out that while one can specify an alternative
listening port using the -a flag, the TLS hostname gets incorrectly
determined and includes the set alternative listening port as part of
the hostname. Thus, the TLS hostname validation always fails when the
-mesh-with values have ports.

Updates #9949

Signed-off-by: Thomas Kosiewski <thomas.kosiewski@loft.sh>
2023-10-24 07:27:29 -07:00
Brad Fitzpatrick
514539b611 wgengine/magicsock: close disco listeners on Conn.Close, fix Linux root TestNewConn
TestNewConn now passes as root on Linux. It wasn't closing the BPF
listeners and their goroutines.

The code is still a mess of two Close overlapping code paths, but that
can be refactored later. For now, make the two close paths more similar.

Updates #9945

Change-Id: I8a3cf5fb04d22ba29094243b8e645de293d9ed85
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-23 19:19:09 -07:00
Andrew Lytvynov
593c086866 clientupdate: distinguish when auto-updates are possible (#9896)
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>
2023-10-23 18:21:54 -07:00
James Tucker
7df6f8736a wgengine/netstack: only add addresses to correct protocols
Prior to an earlier netstack bump this code used a string conversion
path to cover multiple cases of behavior seemingly checking for
unspecified addresses, adding unspecified addresses to v6. The behavior
is now crashy in netstack, as it is enforcing address length in various
areas of the API, one in particular being address removal.

As netstack is now protocol specific, we must not create invalid
protocol addresses - an address is v4 or v6, and the address value
contained inside must match. If a control path attempts to do something
otherwise it is now logged and skipped rather than incorrect addressing
being added.

Fixes tailscale/corp#15377

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-23 17:29:36 -07:00
Tyler Smalley
35d7b3aa27 cmd/tailscale/cli: updates help and background messaging (#9929)
* Fixes issue with template string not being provided in help text
* Updates background information to provide full URL, including path, to make it clear the source and destination
* Restores some tests
* Removes AllowFunnel in ServeConfig if no proxy exists for that port.

updates #8489

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-23 13:24:21 -07:00
Marwan Sulaiman
c53ee37912 cmd/tailscale: add set-raw to the new serve funnel commands
This PR adds the same set-raw from the old flow into the new one
so that users can continue to use it when transitioning into the new
flow.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-23 15:55:13 -04:00
Marwan Sulaiman
f232d4554a cmd/tailscale: translate old serve commands to new ones
This PR fixes the isLegacyInvocation to better catch serve and
funnel legacy commands. In addition, it now also returns a string
that translates the old command into the new one so that users
can have an easier transition story.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-23 15:54:24 -04:00
Sonia Appasamy
62d08d26b6 client/web: set Server.cgiMode field
Updates tailscale/corp#15373

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Co-authored-by: Will Norris <will@tailscale.com>
2023-10-23 15:27:22 -04:00
Maisem Ali
17b2072b72 ipn/ipnlocal: set the push device token correctly
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>
2023-10-23 11:43:25 -07:00
Maisem Ali
0e89245c0f ipn/ipnlocal: drop hostinfo param from doSetHostinfoFilterServices
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>
2023-10-23 11:43:25 -07:00
Joe Tsai
152390e80a ipn/localapi: avoid unkeyed literal (#9933)
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>
2023-10-23 11:15:25 -07:00
Marwan Sulaiman
60e768fd14 cmd/tailscale: allow serve|funnel off to delete an entire port
This PR allows you to do "tailscale serve -bg -https:4545 off" and it
will delete all handlers under it. It will also prompt you for a y/n in case
you wanted to delete a single port.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-10-23 13:42:10 -04:00
Andrew Lytvynov
e561f1ce61 clientupdate: manually restart Windows GUI after update (#9906)
When updating via c2n, `tailscale.exe update` runs from `tailscaled.exe`
which runs as SYSTEM. The MSI installer does not start the GUI when
running as SYSTEM. This results in Tailscale just existing on
auto-update, which is ungood.

Instead, always ask the MSI installer to not launch the GUI (via
`TS_NOLAUNCH` argument) and launch it manually with a token from the
current logged in user. The token code was borrowed from
d9081d6ba2/net/dns/wsl_windows.go (L207-L232)

Also, make some logging changes so that these issues are easier to debug
in the future.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-23 10:24:57 -07:00
Irbe Krumina
e9956419f6 cmd/k8s-operator: allow cleanup of cluster resources for deleted devices (#9917)
Users should delete proxies by deleting or modifying the k8s cluster resources
that they used to tell the operator to create they proxy. With this flow,
the tailscale operator will delete the associated device from the control.
However, in some cases users might have already deleted the device from the control manually.

Updates tailscale/tailscale#9773

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-10-23 16:22:55 +01:00
Brad Fitzpatrick
e87862bce3 go.toolchain.rev: bump Tailscale Go toolchain
Updates tailscale/go#77

Change-Id: I367465fb90cd4369cfbafd913c3964bfe5553dd0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-22 16:20:41 -07:00
Maisem Ali
f398712c00 ipn/ipnlocal: prevent changing serve config if conf.Locked
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>
2023-10-20 21:21:34 -07:00
Tyler Smalley
d9081d6ba2 cmd/tailscale/cli: update serve/funnel CLI help text (#9895)
updates #8489
ENG-2308

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-20 14:17:28 -07:00
Adrian Dewhurst
5347e6a292 control/controlclient: support certstore without cgo
We no longer build Windows releases with cgo enabled, which
automatically turned off certstore support. Rather than re-enabling cgo,
we updated our fork of the certstore package to no longer require cgo.
This updates the package, cleans up how the feature is configured, and
removes the cgo build tag requirement.

Fixes tailscale/corp#14797
Fixes tailscale/coral#118

Change-Id: Iaea34340761c0431d759370532c16a48c0913374
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2023-10-20 15:17:32 -04:00
Sonia Appasamy
68da15516f ipn/localapi,client/web: clean up auth error handling
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>
2023-10-20 14:55:22 -04:00
Andrew Lytvynov
70f9c8a6ed clientupdate: change Mac App Store support (#9891)
In the sandboxed app from the app store, we cannot check
`/Library/Preferences/com.apple.commerce.plist` or run `softwareupdate`.
We can at most print a helpful message and open the app store page.

Also, reenable macsys update function to mark it as supporting c2n
updates. macsys support in `tailscale update` was fixed.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-20 08:58:41 -07:00
Irbe Krumina
eced054796 ipn/ipnlocal: close connections for removed proxy transports (#9884)
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>
2023-10-20 12:04:00 +01:00
Sonia Appasamy
1df2d14c8f client/web: use auth ID in browser sessions
Stores ID from tailcfg.WebClientAuthResponse in browser session
data, and uses ID to hit control server /wait endpoint.

No longer need the control url cached, so removed that from Server.
Also added optional timeNow field, initially to manage time from
tests.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-19 16:32:43 -04:00
Joe Tsai
6ada33db77 taildrop: fix theoretical race condition in fileDeleter.Init (#9876)
It is possible that upon a cold-start, we enqueue a partial file
for deletion that is resumed shortly after startup.

If the file transfer happens to last longer than deleteDelay,
we will delete the partial file, which is unfortunate.
The client spent a long time uploading a file,
only for it to be accidentally deleted.
It's a very rare race, but also a frustrating one
if it happens to manifest.

Fix the code to only delete partial files that
do not have an active puts against it.

We also fix a minor bug in ResumeReader
where we read b[:blockSize] instead of b[:cs.Size].
The former is the fixed size of 64KiB,
while the latter is usually 64KiB,
but may be less for the last block.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-19 13:26:55 -07:00
Andrew Lytvynov
25b6974219 ipn/ipnlocal: send ClientVersion to Apple frontends (#9887)
Apple frontends will now understand this Notify field and handle it.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-19 12:50:21 -07:00
Sonia Appasamy
b4247fabec tailcfg: add ID field to WebClientAuthResponse
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-19 15:13:57 -04:00
Tom DNetto
7e933a8816 appctype: move to types/appctype
Having a types package at the top level was almost certainly unintentional.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15038
2023-10-19 12:00:54 -07:00
Tom DNetto
02908a2d8d appc: implement app connector Server type
This change refactors & moves the bulk of the app connector logic from
./cmd/sniproxy.

A future change will delete the delta in sniproxy and wire it to this type.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15038
2023-10-19 11:35:26 -07:00
Joe Tsai
469b7cabad cmd/tailscale: improve taildrop progress printer on Linux (#9878)
The progress printer was buggy where it would not print correctly
and some of the truncation logic was faulty.

The progress printer now prints something like:

	go1.21.3.linux-amd64.tar.gz               21.53MiB      13.83MiB/s     33.88%    ETA 00:00:03

where it shows
* the number of bytes transferred so far
* the rate of bytes transferred
  (using a 1-second half-life for an exponentially weighted average)
* the progress made as a percentage
* the estimated time
  (as calculated from the rate of bytes transferred)

Other changes:
* It now correctly prints the progress for very small files
* It prints at a faster rate (4Hz instead of 1Hz)
* It uses IEC units for byte quantities
  (to avoid ambiguities of "kb" being kilobits or kilobytes)

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-19 11:04:33 -07:00
Tyler Smalley
7a3ae39025 cmd/tailscale/cli: [serve/funnel] support omitting scheme for TCP (#9856)
The `serve` command for TCP  has always required the scheme of the target to be specified. However, when it's omitted the error message reported is misleading

```
error: failed to apply TCP serve: invalid TCP target "localhost:5900": missing port in address
```

Since we know the target is TCP, we shouldn't require it to be specified. This aligns with the changes for HTTP proxies in https://github.com/tailscale/tailscale/issues/8489

closes #9855

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-19 11:03:06 -07:00
Tyler Smalley
35376d52d4 cmd/tailscale/cli: [serve/funnel] provide correct command for disabling (#9859)
The `off` subcommand removes a serve/funnel for the corresponding type and port. Previously, we were not providing this which would result in an error if someone was using something than the default https=443.

closes #9858

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-19 10:46:44 -07:00
Irbe Krumina
f09cb45f9d ipn/ipnlocal: initiate proxy transport once (#9883)
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>
2023-10-19 18:38:37 +01:00
Sonia Appasamy
73bbf941f8 client/web: hook up auth flow
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>
2023-10-19 13:26:22 -04:00
Irbe Krumina
09b5bb3e55 ipn/ipnlocal: proxy gRPC requests over h2c if needed. (#9847)
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>
2023-10-19 07:12:31 +01:00
Jordan Whited
891d964bd4 wgengine/magicsock: simplify tryEnableUDPOffload() (#9872)
Don't assume Linux lacks UDP_GRO support if it lacks UDP_SEGMENT
support. This mirrors a similar change in wireguard/wireguard-go@177caa7
for consistency sake. We haven't found any issues here, just being
overly paranoid.

Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-10-18 18:50:40 -07:00
Joe Tsai
d603d18956 taildrop: fix TestResume (#9874)
Previously, the test simply relied on:
	defer close()
to cleanup file handles.

This works fine on Unix-based systems,
but not on Windows, which dislikes deleting files
where an open file handle continues to exist.

Fix the test by explicitly closing the file handle
after we are done with the resource.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-18 18:07:30 -07:00
Brad Fitzpatrick
cf27761265 cmd/tsconnect/wasm: add missing tstun.Wrapper.Start call
It's required as of the recent 5297bd2cff.

Updates #7894
Updates #9394 (sure would be nice)

Change-Id: Id6672408dd8a6c82dba71022c8763e589d789fcd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-18 17:24:02 -07:00
Joe Tsai
cb00eac850 taildrop: disable TestResume (#9873)
This test is currently failing on Windows.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-18 15:29:06 -07:00
Joe Tsai
674beabc73 syncs: add Map.LoadFunc (#9869)
The LoadFunc loads a value and calls a user-provided function.
The utility of this method is to ensure that the map lock is held
while executing user-provided logic.
This allows us to solve TOCTOU bugs that would be nearly imposible
to the solve without this API.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-18 15:02:45 -07:00
James Tucker
afb72ecd73 .github/workflows: update golangci-lint
Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-10-18 14:17:35 -07:00
Sonia Appasamy
851536044a client/web: add tests for authorizeRequest
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-18 16:44:25 -04:00
Maisem Ali
c3a8e63100 util/linuxfw: add additional nftable detection logic
We were previously using the netlink API to see if there are chains/rules that
already exist. This works fine in environments where there is either full
nftable support or no support at all. However, we have identified certain
environments which have partial nftable support and the only feasible way of
detecting such an environment is to try to create some of the chains that we
need.

This adds a check to create a dummy postrouting chain which is immediately
deleted. The goal of the check is to ensure we are able to use nftables and
that it won't error out later. This check is only done in the path where we
detected that the system has no preexisting nftable rules.

Updates #5621
Updates #8555
Updates #8762

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-18 13:39:55 -07:00
Maisem Ali
b47cf04624 util/linuxfw: fix broken tests
These tests were broken at HEAD. CI currently does not run these
as root, will figure out how to do that in a followup.

Updates #5621
Updates #8555
Updates #8762

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-18 13:39:55 -07:00
Joe Tsai
a8fbe284b2 taildrop: fix theoretical race condition (#9866)
WaitGroup.Wait should not be concurrently called WaitGroup.Add.
In other words, we should not start new goroutines after shutodwn is called.
Thus, add a conditional to check that shutdown has not been called
before starting off a new waitAndDelete goroutine.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-18 10:21:36 -07:00
License Updater
756a4c43b6 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-10-18 09:43:23 -07:00
Joe Tsai
3f27087e9d taildrop: switch hashing to be streaming based (#9861)
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>
2023-10-17 17:53:40 -07:00
Joe Tsai
7971333603 ipn: fix localapi and peerapi protocol for taildrop resume (#9860)
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>
2023-10-17 16:14:47 -07:00
Andrew Lytvynov
77127a2494 clientupdate: fix background install for linux tarballs (#9852)
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>
2023-10-17 14:24:06 -07:00
Sonia Appasamy
c27870e160 client/web: refactor authorizeRequest
Moves request authorization back into Server.serve to be run at
the start of any request. Fixes Synology unstable track bug where
client would get stuck unable to auth due to not rendering the
Synology redirect auth html on index.html load.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-17 17:16:18 -04:00
Joe Tsai
c2a551469c taildrop: implement asynchronous file deletion (#9844)
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>
2023-10-17 13:46:05 -07:00
Andrew Lytvynov
33bb2bbfe9 tailcfg,cmd/tailscale: add UrgentSecurityUpdate flag to ClientVersion (#9848)
This flag is used in clients to surface urgent updates more prominently.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-17 11:04:44 -07:00
Irbe Krumina
cac290da87 cmd/k8s-operator: users can configure firewall mode for kube operator proxies (#9769)
* cmd/k8s-operator: users can configure operator to set firewall mode for proxies

Users can now pass PROXY_FIREWALL_MODE={nftables,auto,iptables} to operator to make it create ingress/egress proxies with that firewall mode

Also makes sure that if an invalid firewall mode gets configured, the operator will not start provisioning proxy resources, but will instead log an error and write an error event to the related Service.

Updates tailscale/tailscale#9310

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-10-17 18:05:02 +01:00
Tyler Smalley
ddb2a6eb8d cmd/tailscale: promote new serve/funnel CLI to be default (#9833)
The change is being kept to a minimum to make a revert easy if necessary. After the release, we will go back for a final cleanup.

updates #8489

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-17 09:32:17 -07:00
Maisem Ali
f53c3be07c cmd/k8s-operator: use our own container image instead of busybox
We already have sysctl in the `tailscale/tailscale` image, just use that.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-17 08:11:16 -07:00
Brad Fitzpatrick
1fc3573446 ipn/{conffile,ipnlocal}: start booting tailscaled from a config file w/ auth key
Updates #1412

Change-Id: Icd880035a31df59797b8379f4af19da5c4c453e2
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-17 07:12:49 -07:00
Brad Fitzpatrick
6ca8650c7b tstest/tstest: add t.Parallel that can be disabled by TS_SERIAL_TESTS=true
Updates #9841

Change-Id: I1b8f4d6e34ac8540e3b0455a7c79bd400e2721b7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 21:05:59 -07:00
Brad Fitzpatrick
4dec0c6eb9 tstest, tstest/integration, github/workflows: shard integration tests
Over four jobs for now.

Updates #cleanup

Change-Id: Ic2b1a739a454916893945a3f9efc480d6fcbd70b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 20:09:24 -07:00
Maisem Ali
e6ab7d3c14 cmd/testwrapper: parse args better
Previously we were just smushing together args and not trying
to parse the values at all. This resulted in the args to testwrapper
being limited and confusing.

This makes it so that testwrapper parses flags in the exact format as `go test`
command and passes them down in the provided order. It uses tesing.Init to
register flags that `go test` understands, however those are not the only
flags understood by `go test` (such as `-exec`) so we register these separately.

Updates tailscale/corp#14975

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-16 17:33:50 -07:00
Rhea Ghosh
9d3c6bf52e ipn/ipnlocal/peerapi: refactoring taildrop to just one endpoint (#9832)
Updates #14772

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-16 14:35:11 -05:00
Maisem Ali
4899c2c1f4 cmd/containerboot: revert to using tailscale up
This partially reverts commits a61a9ab087
and 7538f38671 and fully reverts
4823a7e591.

The goal of that commit was to reapply known config whenever the
container restarts. However, that already happens when TS_AUTH_ONCE was
false (the default back then). So we only had to selectively reapply the
config if TS_AUTH_ONCE is true, this does exactly that.

This is a little sad that we have to revert to `tailscale up`, but it
fixes the backwards incompatibility problem.

Updates tailscale/tailscale#9539

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-16 12:00:44 -07:00
Andrew Lytvynov
b949e208bb ipn/ipnlocal: fix AllowsUpdate disable after enable (#9827)
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>
2023-10-16 10:54:56 -07:00
Brad Fitzpatrick
18bd98d35b cmd/tailscaled,*: add start of configuration file support
Updates #1412

Co-authored-by: Maisem Ali <maisem@tailscale.com>
Change-Id: I38d559c1784d09fc804f521986c9b4b548718f7d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 10:40:27 -07:00
Rhea Ghosh
71271e41d6 ipn/{ipnlocal/peerapi, localapi} initial taildrop resume api plumbing (#9798)
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>
2023-10-16 12:36:31 -05:00
Brad Fitzpatrick
95faefd1f6 net/dnsfallback: disable recursive resolver for now
It seems to be implicated in a CPU consumption bug that's not yet
understood. Disable it until we understand.

Updates tailscale/corp#15261

Change-Id: Ia6d0c310da6464dda79a70fc3c18be0782812d3f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 09:47:08 -07:00
Andrew Lytvynov
8a5b02133d clientupdate: return ErrUnsupported for macSys clients (#9793)
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>
2023-10-16 09:14:14 -07:00
Flakes Updater
51078b6486 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-16 09:12:16 -07:00
Brad Fitzpatrick
7fd6cc3caa go.mod: bump alexbrainman/sspi
For https://github.com/alexbrainman/sspi/pull/13

Fixes #9131 (hopefully)

Change-Id: I27bb00bbf5e03850f65f18c45f15c4441cc54b23
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 09:10:27 -07:00
Sonia Appasamy
feabb34ea0 ipn/localapi: add debug-web-client endpoint
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>
2023-10-16 10:52:23 -04:00
Kristoffer Dalby
e06f2f1873 ipn/ipnlocal: change serial number policy to be PreferenceOption
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>
2023-10-16 16:01:54 +02:00
Denton Gentry
97ee3891f1 net/dns: use direct when NetworkManager has no systemd-resolved
Endeavour OS, at least, uses NetworkManager 1.44.2 and does
not use systemd-resolved behind the scenes at all. If we
find ourselves in that situation, return "direct" not
"systemd-resolved"

Fixes https://github.com/tailscale/tailscale/issues/9687

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-10-15 17:12:49 -07:00
Brad Fitzpatrick
56ebcd1ed4 .github/workflows: break up race builder a bit more
Move the compilation of everything to its own job too, separate
from test execution.

Updates #7894

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 19:28:31 -07:00
Brad Fitzpatrick
e89927de2b tsnet: fix data race in TestFallbackTCPHandler
Fixes #9805

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 19:12:43 -07:00
Brad Fitzpatrick
18e2936d25 github/workflows: move race tests to their own job
They're slow. Make them their own job that can run in parallel.

Also, only run them in race mode. No need to run them on 386
or non-race amd64.

Updates #7894

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 14:08:58 -07:00
Brad Fitzpatrick
c363b9055d tstest/integration: add tests for tun mode (requiring root)
Updates #7894

Change-Id: Iff0b07b21ae28c712dd665b12918fa28d6f601d0
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 13:52:30 -07:00
Brad Fitzpatrick
a6270826a3 wgengine/magicsock: fix data race regression in disco ping callbacks
Regression from c15997511d. The callback could be run multiple times
from different endpoints.

Fixes #9801

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 13:52:30 -07:00
Maisem Ali
5297bd2cff cmd/tailscaled,net/tstun: fix data race on start-up in TUN mode
Fixes #7894

Change-Id: Ice3f8019405714dd69d02bc07694f3872bb598b8

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-14 08:54:30 -07:00
Brad Fitzpatrick
5c555cdcbb tstest/integration: set race flag when cross compiling, conditionally fail on race
Misc cleanups and things noticed while working on #7894 and pulled out
of a separate change. Submitting them on their own to not distract
from later changes.

Updates #7894

Change-Id: Ie9abc8b88f121c559aeeb7e74db2aa532eb84d3d
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-13 20:01:32 -07:00
Rhea Ghosh
8c7169105e ipn/{ipnlocal/peerapi, localapi}: cleaning up http statuses for consistency and readability (#9796)
Updates #cleanup

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-13 17:40:10 -05:00
Joe Tsai
9cb6c5bb78 util/httphdr: add new package for parsing HTTP headers (#9797)
This adds support for parsing Range and Content-Range headers
according to RFC 7230. The package could be extended in the future
to handle other headers.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-13 15:38:22 -07:00
Andrew Lytvynov
af5a586463 ipn/ipnlocal: include AutoUpdate prefs in HostInfo.AllowsUpdate (#9792)
Updates #9260

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-13 11:14:23 -07:00
Claire Wang
754fb9a8a8 tailcfg: add tailnet field to register request (#9675)
Updates tailscale/corp#10967

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-10-13 14:13:41 -04:00
Joe Tsai
8f948638c5 taildrop: minor cleanups and fixes (#9786)
Perform the same m==nil check in Manager.{PartialFiles,HashPartialFile}
as we do in the other methods.

Fix HashPartialFile is properly handle a length of -1.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-13 10:21:15 -05:00
Joe Tsai
b1867eb23f taildrop: add logic for resuming partial files (#9785)
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>
2023-10-12 16:50:11 -07:00
Maisem Ali
24f322bc43 ipn/ipnlocal: do unexpired cert renewals in the background
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>
2023-10-12 16:02:45 -07:00
Joe Tsai
1a78f240b5 tstime: add DefaultClock (#9691)
In almost every single use of Clock, there is a default behavior
we want to use when the interface is nil,
which is to use the the standard time package.

The Clock interface exists only for testing,
and so tests that care about mocking time
can adequately plumb the the Clock down the stack
and through various data structures.

However, the problem with Clock is that there are many
situations where we really don't care about mocking time
(e.g., measuring execution time for a log message),
where making sure that Clock is non-nil is not worth the burden.
In fact, in a recent refactoring, the biggest pain point was
dealing with nil-interface panics when calling tstime.Clock methods
where mocking time wasn't even needed for the relevant tests.
This required wasted time carefully reviewing the code to
make sure that tstime.Clock was always populated,
and even then we're not statically guaranteed to avoid a nil panic.

Ideally, what we want are default methods on Go interfaces,
but such a language construct does not exist.
However, we can emulate that behavior by declaring
a concrete type that embeds the interface.
If the underlying interface value is nil,
it provides some default behavior (i.e., use StdClock).

This provides us a nice balance of two goals:
* We can plumb tstime.DefaultClock in all relevant places
  for use with mocking time in the tests that care.
* For all other logic that don't care about,
  we never need to worry about whether tstime.DefaultClock
  is nil or not. This is especially relevant in production code
  where we don't want to panic.

Longer-term, we may want to perform a large-scale change
where we rename Clock to ClockInterface
and rename DefaultClock to just Clock.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-12 16:01:17 -07:00
Naman Sood
7783a960e8 client/web: add metric for exit node advertising (#9781)
* client/web: add metric for exit node advertising

Updates tailscale/corp#15215

Signed-off-by: Naman Sood <mail@nsood.in>

* client/web: use http request's context for IncrementCounter

Updates #cleanup

Signed-off-by: Naman Sood <mail@nsood.in>

---------

Signed-off-by: Naman Sood <mail@nsood.in>
2023-10-12 17:02:20 -04:00
James Tucker
ce0830837d appctype: introduce a configuration schema for app connectors
Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-12 10:49:23 -07:00
Joe Tsai
37c646d9d3 taildrop: improve the functionality and reliability of put (#9762)
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>
2023-10-12 09:28:46 -07:00
Maisem Ali
1294b89792 cmd/k8s-operator: allow setting same host value for tls and ingress rules
We were too strict and required the user not specify the host field at all
in the ingress rules, but that degrades compatibility with existing helm charts.

Relax the constraint so that rule.Host can either be empty, or match the tls.Host[0]
value exactly.

Fixes #9548

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-12 06:40:52 -07:00
Maisem Ali
2d4f808a4c cmd/containerboot: fix time based serveConfig watcher
This broke in a last minute refactor and seems to have never worked.

Fixes #9686

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-12 06:36:40 -07:00
James Tucker
4abd470322 tailcfg: implement text encoding for ProtoPortRange
Updates tailscale/corp#15043
Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 23:59:42 -07:00
James Tucker
96f01a73b1 tailcfg: import ProtoPortRange for local use
Imported type and parsing, with minor modifications.

Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 23:46:39 -07:00
Charlotte Brandhorst-Satzkorn
d62af8e643 words: flappy birds, but real life
These birds have been visually identified as having tails. Science
prevails.

Updates tailscale/corp#9599

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-10-11 22:39:30 -07:00
Maisem Ali
1cb9e33a95 cmd/k8s-operator: update env var in manifest to APISERVER_PROXY
Replace the deprecated var with the one in docs to avoid confusion.
Introduced in 335a5aaf9a.

Updates #8317
Fixes #9764

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 21:55:46 -07:00
James Tucker
c1ef55249a types/ipproto: import and test string parsing for ipproto
IPProto has been being converted to and from string formats in multiple
locations with variations in behavior. TextMarshaller and JSONMarshaller
implementations are now added, along with defined accepted and preferred
formats to centralize the logic into a single cross compatible
implementation.

Updates tailscale/corp#15043
Fixes tailscale/corp#15141

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 18:56:33 -07:00
Maisem Ali
319607625f ipn/ipnlocal: fix log spam from now expected paths
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>
2023-10-11 14:53:58 -07:00
Maisem Ali
9d96e05267 net/packet: split off checksum munging into different pkg
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>
2023-10-11 14:25:58 -07:00
Brad Fitzpatrick
8b630c91bc wgengine/filter: use slices.Contains in another place
We keep finding these.

Updates #cleanup

Change-Id: Iabc049b0f8da07341011356f0ecd5315c33ff548
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-11 14:16:52 -07:00
James 'zofrex' Sanderson
0a412eba40 words: Na na na na na na na na na na na na na na na na (#9753)
So gnarly!

Updates tailscale/corp#14698

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2023-10-11 22:15:53 +01:00
James Tucker
11348fbe72 util/nocasemaps: import nocasemaps from corp
This is a dependency of other code being imported later.

Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 13:55:00 -07:00
Maisem Ali
fbfee6a8c0 cmd/containerboot: use linuxfw.NetfilterRunner
This migrates containerboot to reuse the NetfilterRunner used
by tailscaled instead of manipulating iptables rule itself.
This has the added advantage of now working with nftables and
we can potentially drop the `iptables` command from the container
image in the future.

Updates #9310

Co-authored-by: Irbe Krumina <irbe@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 12:23:52 -07:00
Sonia Appasamy
7a0de2997e client/web: remove unused context param from NewServer
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-11 15:06:26 -04:00
Maisem Ali
aad3584319 util/linuxfw: move fake runner into pkg
This allows using the fake runner in different packages
that need to manage filter rules.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 11:48:43 -07:00
Tom DNetto
fffafc65d6 tsnet: support registering fallback TCP flow handlers
For the app connector use-case, it doesnt make sense to use listeners, because then you would
need to register thousands of listeners (for each proto/service/port combo) to handle ranges.

Instead, we plumb through the TCPHandlerForFlow abstraction, to avoid using the listeners
abstraction that would end up being a bit messy.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15038
2023-10-11 11:29:29 -07:00
717 changed files with 64782 additions and 13531 deletions

28
.github/workflows/checklocks.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: checklocks
on:
push:
branches:
- main
pull_request:
paths:
- '**/*.go'
- '.github/workflows/checklocks.yml'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
checklocks:
runs-on: [ ubuntu-latest ]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Build checklocks
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks
- name: Run checklocks vet
# TODO: remove || true once we have applied checklocks annotations everywhere.
run: ./tool/go vet -vettool=/tmp/checklocks ./... || true

View File

@@ -47,6 +47,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
# Install a more recent Go that understands modern go.mod content.
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2

View File

@@ -1,64 +0,0 @@
name: go-licenses
on:
# run action when a change lands in the main branch which updates go.mod or
# our license template file. Also allow manual triggering.
push:
branches:
- main
paths:
- go.mod
- .github/licenses.tmpl
- .github/workflows/go-licenses.yml
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
update-licenses:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
- name: Install go-licenses
run: |
go install github.com/google/go-licenses@v1.2.2-0.20220825154955-5eedde1c6584
- name: Run go-licenses
env:
# include all build tags to include platform-specific dependencies
GOFLAGS: "-tags=android,cgo,darwin,freebsd,ios,js,linux,openbsd,wasm,windows"
run: |
[ -d licenses ] || mkdir licenses
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
- name: Get access token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply+license-updater@tailscale.com>
committer: License Updater <noreply+license-updater@tailscale.com>
branch: licenses/cli
commit-message: "licenses: update tailscale{,d} licenses"
title: "licenses: update tailscale{,d} licenses"
body: Triggered by ${{ github.repository }}@${{ github.sha }}
signoff: true
delete-branch: true
team-reviewers: opensource-license-reviewers

View File

@@ -31,10 +31,10 @@ jobs:
cache: false
- name: golangci-lint
# Note: this is the 'v3' tag as of 2023-04-17
# Note: this is the 'v3' tag as of 2023-08-14
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
with:
version: v1.52.2
version: v1.56
# Show only new issues if it's a pull request.
only-new-issues: true

View File

@@ -22,17 +22,30 @@ jobs:
- name: Scan source code for known vulnerabilities
run: PATH=$PWD/tool/:$PATH "$(./tool/go env GOPATH)/bin/govulncheck" -test ./...
- uses: ruby/action-slack@v3.2.1
with:
payload: >
{
"attachments": [{
"title": "${{ job.status }}: ${{ github.workflow }}",
"title_link": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks",
"text": "${{ github.repository }}@${{ github.sha }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Post to slack
if: failure() && github.event_name == 'schedule'
uses: slackapi/slack-github-action@v1.24.0
env:
SLACK_BOT_TOKEN: ${{ secrets.GOVULNCHECK_BOT_TOKEN }}
with:
channel-id: 'C05PXRM304B'
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Govulncheck failed in ${{ github.repository }}"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "View results"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
}
]
}

31
.github/workflows/kubemanifests.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: "Kubernetes manifests"
on:
pull_request:
paths:
- 'cmd/k8s-operator/**'
- 'k8s-operator/**'
- '.github/workflows/kubemanifests.yaml'
# Cancel workflow run if there is a newer push to the same PR for which it is
# running
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
testchart:
runs-on: [ ubuntu-latest ]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Build and lint Helm chart
run: |
eval `./tool/go run ./cmd/mkversion`
./tool/helm package --app-version="${VERSION_SHORT}" --version=${VERSION_SHORT} './cmd/k8s-operator/deploy/chart'
./tool/helm lint "tailscale-operator-${VERSION_SHORT}.tgz"
- name: Verify that static manifests are up to date
run: |
make kube-generate-all
echo
echo
git diff --name-only --exit-code || (echo "Generated files for Tailscale Kubernetes operator are out of date. Please run 'make kube-generate-all' and commit the diff."; exit 1)

View File

@@ -22,8 +22,7 @@ on:
- "main"
- "release-branch/*"
pull_request:
branches:
- "*"
# all PRs on all branches
merge_group:
branches:
- "main"
@@ -39,14 +38,42 @@ concurrency:
cancel-in-progress: true
jobs:
race-root-integration:
runs-on: ubuntu-22.04
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
matrix:
include:
- shard: '1/4'
- shard: '2/4'
- shard: '3/4'
- shard: '4/4'
steps:
- name: checkout
uses: actions/checkout@v4
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: integration tests as root
run: PATH=$PWD/tool:$PATH /tmp/testwrapper -exec "sudo -E" -race ./tstest/integration/
env:
TS_TEST_SHARD: ${{ matrix.shard }}
test:
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
matrix:
include:
- goarch: amd64
coverflags: "-coverprofile=/tmp/coverage.out"
- goarch: amd64
buildflags: "-race"
shard: '1/3'
- goarch: amd64
buildflags: "-race"
shard: '2/3'
- goarch: amd64
buildflags: "-race"
shard: '3/3'
- goarch: "386" # thanks yaml
runs-on: ubuntu-22.04
steps:
@@ -70,6 +97,7 @@ jobs:
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
- name: build all
if: matrix.buildflags == '' # skip on race builder
run: ./tool/go build ${{matrix.buildflags}} ./...
env:
GOARCH: ${{ matrix.goarch }}
@@ -91,9 +119,15 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ${{matrix.coverflags}} ./... ${{matrix.buildflags}}
env:
GOARCH: ${{ matrix.goarch }}
TS_TEST_SHARD: ${{ matrix.shard }}
- name: Publish to coveralls.io
if: matrix.coverflags != '' # only publish results if we've tracked coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: /tmp/coverage.out
- name: bench all
run: ./tool/go test ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ $(for x in $(git grep -l "^func Benchmark" | xargs dirname | sort | uniq); do echo "./$x"; done)
env:
@@ -149,6 +183,19 @@ jobs:
# the equals signs cause great confusion.
run: go test ./... -bench . -benchtime 1x -run "^$"
privileged:
runs-on: ubuntu-22.04
container:
image: golang:latest
options: --privileged
steps:
- name: checkout
uses: actions/checkout@v4
- name: chown
run: chown -R $(id -u):$(id -g) $PWD
- name: privileged tests
run: ./tool/go test ./util/linuxfw
vm:
runs-on: ["self-hosted", "linux", "vm"]
# VM tests run with some privileges, don't let them run on 3p PRs.
@@ -159,10 +206,20 @@ jobs:
- name: Run VM tests
run: ./tool/go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
env:
HOME: "/tmp"
HOME: "/var/lib/ghrunner/home"
TMPDIR: "/tmp"
XDB_CACHE_HOME: "/var/lib/ghrunner/cache"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
race-build:
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v4
- name: build all
run: ./tool/go install -race ./cmd/...
- name: build tests
run: ./tool/go test -race -exec=true ./...
cross: # cross-compile checks, build only.
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
@@ -197,9 +254,9 @@ jobs:
goarch: amd64
- goos: openbsd
goarch: amd64
# Plan9
- goos: plan9
goarch: amd64
# Plan9 (disabled until 3p dependencies are fixed)
# - goos: plan9
# goarch: amd64
runs-on: ubuntu-22.04
steps:
@@ -385,7 +442,7 @@ jobs:
uses: actions/checkout@v4
- name: check that 'go generate' is clean
run: |
pkgs=$(./tool/go list ./... | grep -v dnsfallback)
pkgs=$(./tool/go list ./... | grep -Ev 'dnsfallback|k8s-operator')
./tool/go generate $pkgs
echo
echo

View File

@@ -0,0 +1,52 @@
name: update-webclient-prebuilt
on:
# manually triggered
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
update-webclient-prebuilt:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Run go get
run: |
./tool/go version # build gocross if needed using regular GOPROXY
GOPROXY=direct ./tool/go get github.com/tailscale/web-client-prebuilt
./tool/go mod tidy
- name: Get access token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
id: generate-token
with:
# TODO(will): this should use the code updater app rather than licensing.
# It has the same permissions, so not a big deal, but still.
app_id: ${{ secrets.LICENSING_APP_ID }}
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
id: pull-request
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
with:
token: ${{ steps.generate-token.outputs.token }}
author: OSS Updater <noreply+oss-updater@tailscale.com>
committer: OSS Updater <noreply+oss-updater@tailscale.com>
branch: actions/update-webclient-prebuilt
commit-message: "go.mod: update web-client-prebuilt module"
title: "go.mod: update web-client-prebuilt module"
body: Triggered by ${{ github.repository }}@${{ github.sha }}
signoff: true
delete-branch: true
reviewers: ${{ github.triggering_actor }}
- name: Summary
if: ${{ steps.pull-request.outputs.pull-request-number }}
run: echo "${{ steps.pull-request.outputs.pull-request-operation}} ${{ steps.pull-request.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY

40
.github/workflows/webclient.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: webclient
on:
workflow_dispatch:
# For now, only run on requests, not the main branches.
pull_request:
branches:
- "*"
paths:
- "client/web/**"
- ".github/workflows/webclient.yml"
- "!**.md"
# TODO(soniaappasamy): enable for main branch after an initial waiting period.
#push:
# branches:
# - main
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
webclient:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install deps
run: ./tool/yarn --cwd client/web
- name: Run lint
run: ./tool/yarn --cwd client/web run --silent lint
- name: Run test
run: ./tool/yarn --cwd client/web run --silent test
- name: Run formatter check
run: |
./tool/yarn --cwd client/web run --silent format-check || ( \
echo "Run this command on your local device to fix the error:" && \
echo "" && \
echo " ./tool/yarn --cwd client/web format" && \
echo "" && exit 1)

View File

@@ -6,6 +6,7 @@ linters:
- bidichk
- gofmt
- goimports
- govet
- misspell
- revive
@@ -35,6 +36,48 @@ linters-settings:
goimports:
govet:
# Matches what we use in corp as of 2023-12-07
enable:
- asmdecl
- assign
- atomic
- bools
- buildtag
- cgocall
- copylocks
- deepequalerrors
- errorsas
- framepointer
- httpresponse
- ifaceassert
- loopclosure
- lostcancel
- nilfunc
- nilness
- printf
- reflectvaluecompare
- shift
- sigchanyzer
- sortslice
- stdmethods
- stringintconv
- structtag
- testinggoroutine
- tests
- unmarshal
- unreachable
- unsafeptr
- unusedresult
settings:
printf:
# List of print function names to check (in addition to default)
funcs:
- github.com/tailscale/tailscale/types/logger.Discard
# NOTE(andrew-d): this doesn't currently work because the printf
# analyzer doesn't support type declarations
#- github.com/tailscale/tailscale/types/logger.Logf
misspell:
revive:

View File

@@ -1 +1 @@
3.16
3.18

View File

@@ -31,7 +31,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.21-alpine AS build-env
FROM golang:1.22-alpine AS build-env
WORKDIR /go/src/tailscale
@@ -66,7 +66,7 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.16
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/

View File

@@ -1,5 +1,5 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.16
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils

View File

@@ -3,19 +3,25 @@ SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
TAGS ?= "latest"
PLATFORM ?= "flyio" ## flyio==linux/amd64. Set to "" to build all platforms.
vet: ## Run go vet
./tool/go vet ./...
tidy: ## Run go mod tidy
./tool/go mod tidy
lint: ## Run golangci-lint
./tool/go run github.com/golangci/golangci-lint/cmd/golangci-lint run
updatedeps: ## Update depaware deps
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
tailscale.com/cmd/derper \
tailscale.com/cmd/stund
depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
@@ -23,7 +29,8 @@ depaware: ## Run depaware checks
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
tailscale.com/cmd/derper \
tailscale.com/cmd/stund
buildwindows: ## Build tailscale CLI for windows/amd64
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
@@ -51,6 +58,21 @@ check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ##
staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
kube-generate-all: kube-generate-deepcopy ## Refresh generated files for Tailscale Kubernetes Operator
./tool/go generate ./cmd/k8s-operator
# Tailscale operator watches Connector custom resources in a Kubernetes cluster
# and caches them locally. Caching is done implicitly by controller-runtime
# library (the middleware used by Tailscale operator to create kube control
# loops). When a Connector resource is GET/LIST-ed from within our control loop,
# the request goes through the cache. To ensure that cache contents don't get
# modified by control loops, controller-runtime deep copies the requested
# object. In order for this to work, Connector must implement deep copy
# functionality so we autogenerate it here.
# https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/cache/internal/cache_reader.go#L86-L89
kube-generate-deepcopy: ## Refresh generated deepcopy functionality for Tailscale kube API types
./scripts/kube-deepcopy.sh
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH}
@@ -68,7 +90,7 @@ publishdevimage: ## Build and publish tailscale image to location specified by $
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@@ -76,7 +98,7 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"

View File

@@ -37,7 +37,7 @@ not open source.
## Building
We always require the latest Go release, currently Go 1.21. (While we build
We always require the latest Go release, currently Go 1.22. (While we build
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
required.)

View File

@@ -1 +1 @@
1.51.0
1.64.0

471
api.md

File diff suppressed because it is too large Load Diff

402
appc/appconnector.go Normal file
View File

@@ -0,0 +1,402 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package appc implements App Connectors.
// An AppConnector provides DNS domain oriented routing of traffic. An App
// Connector becomes a DNS server for a peer, authoritative for the set of
// configured domains. DNS resolution of the target domain triggers dynamic
// publication of routes to ensure that traffic to the domain is routed through
// the App Connector.
package appc
import (
"context"
"net/netip"
"slices"
"strings"
"sync"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/types/logger"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/execqueue"
"tailscale.com/util/mak"
)
// RouteAdvertiser is an interface that allows the AppConnector to advertise
// newly discovered routes that need to be served through the AppConnector.
type RouteAdvertiser interface {
// AdvertiseRoute adds one or more route advertisements skipping any that
// are already advertised.
AdvertiseRoute(...netip.Prefix) error
// UnadvertiseRoute removes any matching route advertisements.
UnadvertiseRoute(...netip.Prefix) error
}
// AppConnector is an implementation of an AppConnector that performs
// its function as a subsystem inside of a tailscale node. At the control plane
// side App Connector routing is configured in terms of domains rather than IP
// addresses.
// The AppConnectors responsibility inside tailscaled is to apply the routing
// and domain configuration as supplied in the map response.
// DNS requests for configured domains are observed. If the domains resolve to
// routes not yet served by the AppConnector the local node configuration is
// updated to advertise the new route.
type AppConnector struct {
logf logger.Logf
routeAdvertiser RouteAdvertiser
// mu guards the fields that follow
mu sync.Mutex
// domains is a map of lower case domain names with no trailing dot, to an
// ordered list of resolved IP addresses.
domains map[string][]netip.Addr
// controlRoutes is the list of routes that were last supplied by control.
controlRoutes []netip.Prefix
// wildcards is the list of domain strings that match subdomains.
wildcards []string
// queue provides ordering for update operations
queue execqueue.ExecQueue
}
// NewAppConnector creates a new AppConnector.
func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser) *AppConnector {
return &AppConnector{
logf: logger.WithPrefix(logf, "appc: "),
routeAdvertiser: routeAdvertiser,
}
}
// UpdateDomainsAndRoutes starts an asynchronous update of the configuration
// given the new domains and routes.
func (e *AppConnector) UpdateDomainsAndRoutes(domains []string, routes []netip.Prefix) {
e.queue.Add(func() {
// Add the new routes first.
e.updateRoutes(routes)
e.updateDomains(domains)
})
}
// UpdateDomains asynchronously replaces the current set of configured domains
// with the supplied set of domains. Domains must not contain a trailing dot,
// and should be lower case. If the domain contains a leading '*' label it
// matches all subdomains of a domain.
func (e *AppConnector) UpdateDomains(domains []string) {
e.queue.Add(func() {
e.updateDomains(domains)
})
}
// Wait waits for the currently scheduled asynchronous configuration changes to
// complete.
func (e *AppConnector) Wait(ctx context.Context) {
e.queue.Wait(ctx)
}
func (e *AppConnector) updateDomains(domains []string) {
e.mu.Lock()
defer e.mu.Unlock()
var oldDomains map[string][]netip.Addr
oldDomains, e.domains = e.domains, make(map[string][]netip.Addr, len(domains))
e.wildcards = e.wildcards[:0]
for _, d := range domains {
d = strings.ToLower(d)
if len(d) == 0 {
continue
}
if strings.HasPrefix(d, "*.") {
e.wildcards = append(e.wildcards, d[2:])
continue
}
e.domains[d] = oldDomains[d]
delete(oldDomains, d)
}
// Ensure that still-live wildcards addresses are preserved as well.
for d, addrs := range oldDomains {
for _, wc := range e.wildcards {
if dnsname.HasSuffix(d, wc) {
e.domains[d] = addrs
break
}
}
}
e.logf("handling domains: %v and wildcards: %v", xmaps.Keys(e.domains), e.wildcards)
}
// updateRoutes merges the supplied routes into the currently configured routes. The routes supplied
// by control for UpdateRoutes are supplemental to the routes discovered by DNS resolution, but are
// also more often whole ranges. UpdateRoutes will remove any single address routes that are now
// covered by new ranges.
func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
e.mu.Lock()
defer e.mu.Unlock()
// If there was no change since the last update, no work to do.
if slices.Equal(e.controlRoutes, routes) {
return
}
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
return
}
var toRemove []netip.Prefix
nextRoute:
for _, r := range routes {
for _, addr := range e.domains {
for _, a := range addr {
if r.Contains(a) && netip.PrefixFrom(a, a.BitLen()) != r {
pfx := netip.PrefixFrom(a, a.BitLen())
toRemove = append(toRemove, pfx)
continue nextRoute
}
}
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
e.controlRoutes = routes
}
// Domains returns the currently configured domain list.
func (e *AppConnector) Domains() views.Slice[string] {
e.mu.Lock()
defer e.mu.Unlock()
return views.SliceOf(xmaps.Keys(e.domains))
}
// DomainRoutes returns a map of domains to resolved IP
// addresses.
func (e *AppConnector) DomainRoutes() map[string][]netip.Addr {
e.mu.Lock()
defer e.mu.Unlock()
drCopy := make(map[string][]netip.Addr)
for k, v := range e.domains {
drCopy[k] = append(drCopy[k], v...)
}
return drCopy
}
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
// response is being returned over the PeerAPI. The response is parsed and
// matched against the configured domains, if matched the routeAdvertiser is
// advised to advertise the discovered route.
func (e *AppConnector) ObserveDNSResponse(res []byte) {
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return
}
if err := p.SkipAllQuestions(); err != nil {
return
}
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
// a CNAME chain back to the original query for flattening. The keys are
// CNAME record targets, and the value is the name the record answers, so
// for www.example.com CNAME example.com, the map would contain
// ["example.com"] = "www.example.com".
var cnameChain map[string]string
// addressRecords is a list of address records found in the response.
var addressRecords map[string][]netip.Addr
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return
}
if h.Class != dnsmessage.ClassINET {
if err := p.SkipAnswer(); err != nil {
return
}
continue
}
switch h.Type {
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
default:
if err := p.SkipAnswer(); err != nil {
return
}
continue
}
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
if len(domain) == 0 {
continue
}
if h.Type == dnsmessage.TypeCNAME {
res, err := p.CNAMEResource()
if err != nil {
return
}
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
if len(cname) == 0 {
continue
}
mak.Set(&cnameChain, cname, domain)
continue
}
switch h.Type {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return
}
addr := netip.AddrFrom4(r.A)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return
}
addr := netip.AddrFrom16(r.AAAA)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
default:
if err := p.SkipAnswer(); err != nil {
return
}
continue
}
}
e.mu.Lock()
defer e.mu.Unlock()
for domain, addrs := range addressRecords {
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
// domain and none of the CNAMEs in the chain are routed
if !isRouted {
continue
}
// advertise each address we have learned for the routed domain, that
// was not already known.
var toAdvertise []netip.Prefix
for _, addr := range addrs {
if !e.isAddrKnownLocked(domain, addr) {
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
}
}
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
e.scheduleAdvertisement(domain, toAdvertise...)
}
}
// starting from the given domain that resolved to an address, find it, or any
// of the domains in the CNAME chain toward resolving it, that are routed
// domains, returning the routed domain name and a bool indicating whether a
// routed domain was found.
// e.mu must be held.
func (e *AppConnector) findRoutedDomainLocked(domain string, cnameChain map[string]string) (string, bool) {
var isRouted bool
for {
_, isRouted = e.domains[domain]
if isRouted {
break
}
// match wildcard domains
for _, wc := range e.wildcards {
if dnsname.HasSuffix(domain, wc) {
e.domains[domain] = nil
isRouted = true
break
}
}
next, ok := cnameChain[domain]
if !ok {
break
}
domain = next
}
return domain, isRouted
}
// isAddrKnownLocked returns true if the address is known to be associated with
// the given domain. Known domain tables are updated for covered routes to speed
// up future matches.
// e.mu must be held.
func (e *AppConnector) isAddrKnownLocked(domain string, addr netip.Addr) bool {
if e.hasDomainAddrLocked(domain, addr) {
return true
}
for _, route := range e.controlRoutes {
if route.Contains(addr) {
// record the new address associated with the domain for faster matching in subsequent
// requests and for diagnostic records.
e.addDomainAddrLocked(domain, addr)
return true
}
}
return false
}
// scheduleAdvertisement schedules an advertisement of the given address
// associated with the given domain.
func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Prefix) {
e.queue.Add(func() {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err)
return
}
e.mu.Lock()
defer e.mu.Unlock()
for _, route := range routes {
if !route.IsSingleIP() {
continue
}
addr := route.Addr()
if !e.hasDomainAddrLocked(domain, addr) {
e.addDomainAddrLocked(domain, addr)
e.logf("[v2] advertised route for %v: %v", domain, addr)
}
}
})
}
// hasDomainAddrLocked returns true if the address has been observed in a
// resolution of domain.
func (e *AppConnector) hasDomainAddrLocked(domain string, addr netip.Addr) bool {
_, ok := slices.BinarySearchFunc(e.domains[domain], addr, compareAddr)
return ok
}
// addDomainAddrLocked adds the address to the list of addresses resolved for
// domain and ensures the list remains sorted. Does not attempt to deduplicate.
func (e *AppConnector) addDomainAddrLocked(domain string, addr netip.Addr) {
e.domains[domain] = append(e.domains[domain], addr)
slices.SortFunc(e.domains[domain], compareAddr)
}
func compareAddr(l, r netip.Addr) int {
return l.Compare(r)
}

312
appc/appconnector_test.go Normal file
View File

@@ -0,0 +1,312 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package appc
import (
"context"
"net/netip"
"reflect"
"slices"
"testing"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc/appctest"
"tailscale.com/util/mak"
"tailscale.com/util/must"
)
func TestUpdateDomains(t *testing.T) {
ctx := context.Background()
a := NewAppConnector(t.Logf, nil)
a.UpdateDomains([]string{"example.com"})
a.Wait(ctx)
if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
addr := netip.MustParseAddr("192.0.0.8")
a.domains["example.com"] = append(a.domains["example.com"], addr)
a.UpdateDomains([]string{"example.com"})
a.Wait(ctx)
if got, want := a.domains["example.com"], []netip.Addr{addr}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
// domains are explicitly downcased on set.
a.UpdateDomains([]string{"UP.EXAMPLE.COM"})
a.Wait(ctx)
if got, want := xmaps.Keys(a.domains), []string{"up.example.com"}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
}
func TestUpdateRoutes(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.updateDomains([]string{"*.example.com"})
// This route should be collapsed into the range
a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.0.2.1"))
a.Wait(ctx)
if !slices.Equal(rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}) {
t.Fatalf("got %v, want %v", rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
}
// This route should not be collapsed or removed
a.ObserveDNSResponse(dnsResponse("b.example.com.", "192.0.0.1"))
a.Wait(ctx)
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
a.updateRoutes(routes)
slices.SortFunc(rc.Routes(), prefixCompare)
rc.SetRoutes(slices.Compact(rc.Routes()))
slices.SortFunc(routes, prefixCompare)
// Ensure that the non-matching /32 is preserved, even though it's in the domains table.
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Errorf("added routes: got %v, want %v", rc.Routes(), routes)
}
// Ensure that the contained /32 is removed, replaced by the /24.
wantRemoved := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}
if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) {
t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes())
}
}
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")})
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
a.updateRoutes(routes)
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes)
}
}
func TestDomainRoutes(t *testing.T) {
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.updateDomains([]string{"example.com"})
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
a.Wait(context.Background())
want := map[string][]netip.Addr{
"example.com": {netip.MustParseAddr("192.0.0.8")},
}
if got := a.DomainRoutes(); !reflect.DeepEqual(got, want) {
t.Fatalf("DomainRoutes: got %v, want %v", got, want)
}
}
func TestObserveDNSResponse(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
// a has no domains configured, so it should not advertise any routes
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
if got, want := rc.Routes(), ([]netip.Prefix)(nil); !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
a.updateDomains([]string{"example.com"})
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
a.Wait(ctx)
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
// a CNAME record chain should result in a route being added if the chain
// matches a routed domain.
a.updateDomains([]string{"www.example.com", "example.com"})
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com."))
a.Wait(ctx)
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.9/32"))
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
// a CNAME record chain should result in a route being added if the chain
// even if only found in the middle of the chain
a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.10", "outside.example.org.", "www.example.com.", "example.org."))
a.Wait(ctx)
wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.10/32"))
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
a.Wait(ctx)
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
// don't re-advertise routes that have already been advertised
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
a.Wait(ctx)
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
}
// don't advertise addresses that are already in a control provided route
pfx := netip.MustParsePrefix("192.0.2.0/24")
a.updateRoutes([]netip.Prefix{pfx})
wantRoutes = append(wantRoutes, pfx)
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1"))
a.Wait(ctx)
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
}
if !slices.Contains(a.domains["example.com"], netip.MustParseAddr("192.0.2.1")) {
t.Errorf("missing %v from %v", "192.0.2.1", a.domains["exmaple.com"])
}
}
func TestWildcardDomains(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.updateDomains([]string{"*.example.com"})
a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8"))
a.Wait(ctx)
if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
t.Errorf("routes: got %v; want %v", got, want)
}
if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("wildcards: got %v; want %v", got, want)
}
a.updateDomains([]string{"*.example.com", "example.com"})
if _, ok := a.domains["foo.example.com"]; !ok {
t.Errorf("expected foo.example.com to be preserved in domains due to wildcard")
}
if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("wildcards: got %v; want %v", got, want)
}
// There was an early regression where the wildcard domain was added repeatedly, this guards against that.
a.updateDomains([]string{"*.example.com", "example.com"})
if len(a.wildcards) != 1 {
t.Errorf("expected only one wildcard domain, got %v", a.wildcards)
}
}
// dnsResponse is a test helper that creates a DNS response buffer for the given domain and address
func dnsResponse(domain, address string) []byte {
addr := netip.MustParseAddr(address)
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
b.EnableCompression()
b.StartAnswers()
switch addr.BitLen() {
case 32:
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: addr.As4(),
},
)
case 128:
b.AAAAResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AAAAResource{
AAAA: addr.As16(),
},
)
default:
panic("invalid address length")
}
return must.Get(b.Finish())
}
func dnsCNAMEResponse(address string, domains ...string) []byte {
addr := netip.MustParseAddr(address)
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
b.EnableCompression()
b.StartAnswers()
if len(domains) >= 2 {
for i, domain := range domains[:len(domains)-1] {
b.CNAMEResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeCNAME,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.CNAMEResource{
CNAME: dnsmessage.MustNewName(domains[i+1]),
},
)
}
}
domain := domains[len(domains)-1]
switch addr.BitLen() {
case 32:
b.AResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AResource{
A: addr.As4(),
},
)
case 128:
b.AAAAResource(
dnsmessage.ResourceHeader{
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
TTL: 0,
},
dnsmessage.AAAAResource{
AAAA: addr.As16(),
},
)
default:
panic("invalid address length")
}
return must.Get(b.Finish())
}
func prefixEqual(a, b netip.Prefix) bool {
return a == b
}
func prefixCompare(a, b netip.Prefix) int {
if a.Addr().Compare(b.Addr()) == 0 {
return a.Bits() - b.Bits()
}
return a.Addr().Compare(b.Addr())
}

49
appc/appctest/appctest.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package appctest
import (
"net/netip"
"slices"
)
// RouteCollector is a test helper that collects the list of routes advertised
type RouteCollector struct {
routes []netip.Prefix
removedRoutes []netip.Prefix
}
func (rc *RouteCollector) AdvertiseRoute(pfx ...netip.Prefix) error {
rc.routes = append(rc.routes, pfx...)
return nil
}
func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error {
routes := rc.routes
rc.routes = rc.routes[:0]
for _, r := range routes {
if !slices.Contains(toRemove, r) {
rc.routes = append(rc.routes, r)
} else {
rc.removedRoutes = append(rc.removedRoutes, r)
}
}
return nil
}
// RemovedRoutes returns the list of routes that were removed.
func (rc *RouteCollector) RemovedRoutes() []netip.Prefix {
return rc.removedRoutes
}
// Routes returns the ordered list of routes that were added, including
// possible duplicates.
func (rc *RouteCollector) Routes() []netip.Prefix {
return rc.routes
}
func (rc *RouteCollector) SetRoutes(routes []netip.Prefix) error {
rc.routes = routes
return nil
}

View File

@@ -20,18 +20,19 @@
set -eu
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
export PATH=$PWD/tool:$PATH
export PATH="$PWD"/tool:"$PATH"
eval $(./build_dist.sh shellvars)
eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.16"
DEFAULT_BASE="tailscale/alpine-base:3.18"
PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
case "$TARGET" in
client)
@@ -50,6 +51,7 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/containerboot
;;
operator)
@@ -65,10 +67,11 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/operator
;;
*)
echo "unknown target: $TARGET"
exit 1
;;
esac
esac

View File

@@ -270,6 +270,14 @@ type UserRuleMatch struct {
Users []string `json:"users"`
Ports []string `json:"ports"`
LineNumber int `json:"lineNumber"`
// Postures is a list of posture policies that are
// associated with this match. The rules can be looked
// up in the ACLPreviewResponse parent struct.
// The source of the list is from srcPosture on
// an ACL or Grant rule:
// https://tailscale.com/kb/1288/device-posture#posture-conditions
Postures []string `json:"postures"`
}
// ACLPreviewResponse is the response type of previewACLPostRequest
@@ -277,6 +285,12 @@ type ACLPreviewResponse struct {
Matches []UserRuleMatch `json:"matches"` // ACL rules that match the specified user or ipport.
Type string `json:"type"` // The request type: currently only "user" or "ipport".
PreviewFor string `json:"previewFor"` // A specific user or ipport.
// Postures is a map of postures and associated rules that apply
// to this preview.
// For more details about the posture mapping, see:
// https://tailscale.com/kb/1288/device-posture#postures
Postures map[string][]string `json:"postures,omitempty"`
}
// ACLPreview is the response type of PreviewACLForUser, PreviewACLForIPPort, PreviewACLHuJSONForUser, and PreviewACLHuJSONForIPPort
@@ -284,6 +298,12 @@ type ACLPreview struct {
Matches []UserRuleMatch `json:"matches"`
User string `json:"user,omitempty"` // Filled if response of PreviewACLForUser or PreviewACLHuJSONForUser
IPPort string `json:"ipport,omitempty"` // Filled if response of PreviewACLForIPPort or PreviewACLHuJSONForIPPort
// Postures is a map of postures and associated rules that apply
// to this preview.
// For more details about the posture mapping, see:
// https://tailscale.com/kb/1288/device-posture#postures
Postures map[string][]string `json:"postures,omitempty"`
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
@@ -341,8 +361,9 @@ func (c *Client) PreviewACLForUser(ctx context.Context, acl ACL, user string) (r
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
Matches: b.Matches,
User: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -369,8 +390,9 @@ func (c *Client) PreviewACLForIPPort(ctx context.Context, acl ACL, ipport netip.
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
Matches: b.Matches,
IPPort: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -394,8 +416,9 @@ func (c *Client) PreviewACLHuJSONForUser(ctx context.Context, acl ACLHuJSON, use
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
Matches: b.Matches,
User: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -419,8 +442,9 @@ func (c *Client) PreviewACLHuJSONForIPPort(ctx context.Context, acl ACLHuJSON, i
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
Matches: b.Matches,
IPPort: b.PreviewFor,
Postures: b.Postures,
}, nil
}

View File

@@ -40,3 +40,12 @@ type SetPushDeviceTokenRequest struct {
// PushDeviceToken is the iOS/macOS APNs device token (and any future Android equivalent).
PushDeviceToken string
}
// ReloadConfigResponse is the response to a LocalAPI reload-config request.
//
// There are three possible outcomes: (false, "") if no config mode in use,
// (true, "") on success, or (false, "error message") on failure.
type ReloadConfigResponse struct {
Reloaded bool // whether the config was reloaded
Err string // any error message
}

View File

@@ -71,6 +71,17 @@ type Device struct {
AdvertisedRoutes []string `json:"advertisedRoutes"` // Empty for external devices.
ClientConnectivity *ClientConnectivity `json:"clientConnectivity"`
// PostureIdentity contains extra identifiers collected from the device when
// the tailnet has the device posture identification features enabled. If
// Tailscale have attempted to collect this from the device but it has not
// opted in, PostureIdentity will have Disabled=true.
PostureIdentity *DevicePostureIdentity `json:"postureIdentity"`
}
type DevicePostureIdentity struct {
Disabled bool `json:"disabled,omitempty"`
SerialNumbers []string `json:"serialNumbers,omitempty"`
}
// DeviceFieldsOpts determines which fields should be returned in the response.

View File

@@ -7,6 +7,7 @@ package tailscale
import (
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/json"
@@ -27,6 +28,7 @@ import (
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/drive"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -37,7 +39,6 @@ import (
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
"tailscale.com/util/cmpx"
)
// defaultLocalClient is the default LocalClient when using the legacy
@@ -102,8 +103,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
return safesocket.Connect(s)
return safesocket.Connect(lc.socket())
}
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
@@ -480,7 +480,7 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts)
opts = &DebugPortmapOpts{}
}
vals.Set("duration", cmpx.Or(opts.Duration, 5*time.Second).String())
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
vals.Set("type", opts.Type)
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
@@ -679,6 +679,26 @@ func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
return nil
}
// CheckUDPGROForwarding asks the local Tailscale daemon whether it looks like
// the machine is optimally configured to forward UDP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-udp-gro-forwarding")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-udp-gro-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
// CheckPrefs validates the provided preferences, without making any changes.
//
// The CLI uses this before a Start call to fail fast if the preferences won't
@@ -1244,6 +1264,22 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
return current, all, err
}
// ReloadConfig reloads the config file, if possible.
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
if err != nil {
return
}
res, err := decodeJSON[apitype.ReloadConfigResponse](body)
if err != nil {
return
}
if res.Err != "" {
return false, errors.New(res.Err)
}
return res.Reloaded, nil
}
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
// profile is not assigned an ID until it is persisted after a successful login.
// In order to login to the new profile, the user must call LoginInteractive.
@@ -1296,6 +1332,15 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin
return decodeJSON[*ipnstate.DebugDERPRegionReport](body)
}
// DebugPacketFilterRules returns the packet filter rules for the current device.
func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[[]tailcfg.FilterRule](body)
}
// DebugSetExpireIn marks the current node key to expire in d.
//
// This is meant primarily for debug and testing.
@@ -1358,6 +1403,81 @@ func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt)
}, nil
}
// CheckUpdate returns a tailcfg.ClientVersion indicating whether or not an update is available
// to be installed via the LocalAPI. In case the LocalAPI can't install updates, it returns a
// ClientVersion that says that we are up to date.
func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
body, err := lc.get200(ctx, "/localapi/v0/update/check")
if err != nil {
return nil, err
}
cv, err := decodeJSON[tailcfg.ClientVersion](body)
if err != nil {
return nil, err
}
return &cv, nil
}
// SetUseExitNode toggles the use of an exit node on or off.
// To turn it on, there must have been a previously used exit node.
// The most previously used one is reused.
// This is a convenience method for GUIs. To select an actual one, update the prefs.
func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
return err
}
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// Taildrive know to use the file server running in the GUI app.
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err
}
// DriveShareSet adds or updates the given share in the list of shares that
// Taildrive will serve to remote nodes. If a share with the same name already
// exists, the existing share is replaced/updated.
func (lc *LocalClient) DriveShareSet(ctx context.Context, share *drive.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
return err
}
// DriveShareRemove removes the share with the given name from the list of
// shares that Taildrive will serve to remote nodes.
func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error {
_, err := lc.send(
ctx,
"DELETE",
"/localapi/v0/drive/shares",
http.StatusNoContent,
strings.NewReader(name))
return err
}
// DriveShareRename renames the share from old to new name.
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
_, err := lc.send(
ctx,
"POST",
"/localapi/v0/drive/shares",
http.StatusNoContent,
jsonBody([2]string{oldName, newName}))
return err
}
// DriveShareList returns the list of shares that drive is currently serving
// to remote nodes.
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
if err != nil {
return nil, err
}
var shares []*drive.Share
err = json.Unmarshal(result, &shares)
return shares, err
}
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
// It's returned by LocalClient.WatchIPNBus.
//

View File

@@ -5,7 +5,11 @@
package tailscale
import "testing"
import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestGetServeConfigFromJSON(t *testing.T) {
sc, err := getServeConfigFromJSON([]byte("null"))
@@ -25,3 +29,14 @@ func TestGetServeConfigFromJSON(t *testing.T) {
t.Errorf("want non-nil TCP for object")
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{
// Make sure we don't again accidentally bring in a dependency on
// drive or its transitive dependencies
"tailscale.com/drive/driveimpl": "https://github.com/tailscale/tailscale/pull/10631",
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
},
}.Check(t)
}

View File

@@ -4,6 +4,8 @@
package web
import (
"io"
"io/fs"
"log"
"net/http"
"net/http/httputil"
@@ -12,17 +14,61 @@ import (
"os/exec"
"path/filepath"
"strings"
"time"
prebuilt "github.com/tailscale/web-client-prebuilt"
)
var start = time.Now()
func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
if devMode {
// When in dev mode, proxy asset requests to the Vite dev server.
cleanup := startDevServer()
return devServerProxy(), cleanup
}
return http.FileServer(http.FS(prebuilt.FS())), nil
fsys := prebuilt.FS()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
f, err := openPrecompressedFile(w, r, path, fsys)
if err != nil {
// Rewrite request to just fetch index.html and let
// the frontend router handle it.
r = r.Clone(r.Context())
path = "index.html"
f, err = openPrecompressedFile(w, r, path, fsys)
}
if f == nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
defer f.Close()
// fs.File does not claim to implement Seeker, but in practice it does.
fSeeker, ok := f.(io.ReadSeeker)
if !ok {
http.Error(w, "Not seekable", http.StatusInternalServerError)
return
}
if strings.HasPrefix(path, "assets/") {
// Aggressively cache static assets, since we cache-bust our assets with
// hashed filenames.
w.Header().Set("Cache-Control", "public, max-age=31535996")
w.Header().Set("Vary", "Accept-Encoding")
}
http.ServeContent(w, r, path, start, fSeeker)
}), nil
}
func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) {
if f, err := fs.Open(path + ".gz"); err == nil {
w.Header().Set("Content-Encoding", "gzip")
return f, nil
}
return fs.Open(path) // fallback
}
// startDevServer starts the JS dev server that does on-demand rebuilding
@@ -35,7 +81,7 @@ func startDevServer() (cleanup func()) {
node := filepath.Join(root, "tool", "node")
vite := filepath.Join(webClientPath, "node_modules", ".bin", "vite")
log.Printf("installing JavaScript deps using %s... (might take ~30s)", yarn)
log.Printf("installing JavaScript deps using %s...", yarn)
out, err := exec.Command(yarn, "--non-interactive", "-s", "--cwd", webClientPath, "install").CombinedOutput()
if err != nil {
log.Fatalf("error running tailscale web's yarn install: %v, %s", err, out)

339
client/web/auth.go Normal file
View File

@@ -0,0 +1,339 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package web
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
const (
sessionCookieName = "TS-Web-Session"
sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
)
// browserSession holds data about a user's browser session
// on the full management web client.
type browserSession struct {
// ID is the unique identifier for the session.
// It is passed in the user's "TS-Web-Session" browser cookie.
ID string
SrcNode tailcfg.NodeID
SrcUser tailcfg.UserID
AuthID string // from tailcfg.WebClientAuthResponse
AuthURL string // from tailcfg.WebClientAuthResponse
Created time.Time
Authenticated bool
}
// isAuthorized reports true if the given session is authorized
// to be used by its associated user to access the full management
// web client.
//
// isAuthorized is true only when s.Authenticated is true (i.e.
// the user has authenticated the session) and the session is not
// expired.
// 2023-10-05: Sessions expire by default 30 days after creation.
func (s *browserSession) isAuthorized(now time.Time) bool {
switch {
case s == nil:
return false
case !s.Authenticated:
return false // awaiting auth
case s.isExpired(now):
return false // expired
}
return true
}
// isExpired reports true if s is expired.
// 2023-10-05: Sessions expire by default 30 days after creation.
func (s *browserSession) isExpired(now time.Time) bool {
return !s.Created.IsZero() && now.After(s.expires())
}
// expires reports when the given session expires.
func (s *browserSession) expires() time.Time {
return s.Created.Add(sessionCookieExpiry)
}
var (
errNoSession = errors.New("no-browser-session")
errNotUsingTailscale = errors.New("not-using-tailscale")
errTaggedRemoteSource = errors.New("tagged-remote-source")
errTaggedLocalSource = errors.New("tagged-local-source")
errNotOwner = errors.New("not-owner")
)
// getSession retrieves the browser session associated with the request,
// if one exists.
//
// An error is returned in any of the following cases:
//
// - (errNotUsingTailscale) The request was not made over tailscale.
//
// - (errNoSession) The request does not have a session.
//
// - (errTaggedRemoteSource) The source is remote (another node) and tagged.
// Users must use their own user-owned devices to manage other nodes'
// web clients.
//
// - (errTaggedLocalSource) The source is local (the same node) and tagged.
// Tagged nodes can only be remotely managed, allowing ACLs to dictate
// access to web clients.
//
// - (errNotOwner) The source is not the owner of this client (if the
// client is user-owned). Only the owner is allowed to manage the
// node via the web client.
//
// If no error is returned, the browserSession is always non-nil.
// getTailscaleBrowserSession does not check whether the session has been
// authorized by the user. Callers can use browserSession.isAuthorized.
//
// The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
// unless getTailscaleBrowserSession reports errNotUsingTailscale.
func (s *Server) getSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, *ipnstate.Status, error) {
whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
status, statusErr := s.lc.StatusWithoutPeers(r.Context())
switch {
case whoIsErr != nil:
return nil, nil, status, errNotUsingTailscale
case statusErr != nil:
return nil, whoIs, nil, statusErr
case status.Self == nil:
return nil, whoIs, status, errors.New("missing self node in tailscale status")
case whoIs.Node.IsTagged() && whoIs.Node.StableID == status.Self.ID:
return nil, whoIs, status, errTaggedLocalSource
case whoIs.Node.IsTagged():
return nil, whoIs, status, errTaggedRemoteSource
case !status.Self.IsTagged() && status.Self.UserID != whoIs.UserProfile.ID:
return nil, whoIs, status, errNotOwner
}
srcNode := whoIs.Node.ID
srcUser := whoIs.UserProfile.ID
cookie, err := r.Cookie(sessionCookieName)
if errors.Is(err, http.ErrNoCookie) {
return nil, whoIs, status, errNoSession
} else if err != nil {
return nil, whoIs, status, err
}
v, ok := s.browserSessions.Load(cookie.Value)
if !ok {
return nil, whoIs, status, errNoSession
}
session := v.(*browserSession)
if session.SrcNode != srcNode || session.SrcUser != srcUser {
// In this case the browser cookie is associated with another tailscale node.
// Maybe the source browser's machine was logged out and then back in as a different node.
// Return errNoSession because there is no session for this user.
return nil, whoIs, status, errNoSession
} else if session.isExpired(s.timeNow()) {
// Session expired, remove from session map and return errNoSession.
s.browserSessions.Delete(session.ID)
return nil, whoIs, status, errNoSession
}
return session, whoIs, status, nil
}
// newSession creates a new session associated with the given source user/node,
// and stores it back to the session cache. Creating of a new session includes
// generating a new auth URL from the control server.
func (s *Server) newSession(ctx context.Context, src *apitype.WhoIsResponse) (*browserSession, error) {
sid, err := s.newSessionID()
if err != nil {
return nil, err
}
session := &browserSession{
ID: sid,
SrcNode: src.Node.ID,
SrcUser: src.UserProfile.ID,
Created: s.timeNow(),
}
if s.controlSupportsCheckMode(ctx) {
// control supports check mode, so get a new auth URL and return.
a, err := s.newAuthURL(ctx, src.Node.ID)
if err != nil {
return nil, err
}
session.AuthID = a.ID
session.AuthURL = a.URL
} else {
// control does not support check mode, so there is no additional auth we can do.
session.Authenticated = true
}
s.browserSessions.Store(sid, session)
return session, nil
}
// controlSupportsCheckMode returns whether the current control server supports web client check mode, to verify a user's identity.
// We assume that only "tailscale.com" control servers support check mode.
// This allows the web client to be used with non-standard control servers.
// If an error occurs getting the control URL, this method returns true to fail closed.
//
// TODO(juanfont/headscale#1623): adjust or remove this when headscale supports check mode.
func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
prefs, err := s.lc.GetPrefs(ctx)
if err != nil {
return true
}
controlURL, err := url.Parse(prefs.ControlURLOrDefault())
if err != nil {
return true
}
return strings.HasSuffix(controlURL.Host, ".tailscale.com")
}
// awaitUserAuth blocks until the given session auth has been completed
// by the user on the control server, then updates the session cache upon
// completion. An error is returned if control auth failed for any reason.
func (s *Server) awaitUserAuth(ctx context.Context, session *browserSession) error {
if session.isAuthorized(s.timeNow()) {
return nil // already authorized
}
a, err := s.waitAuthURL(ctx, session.AuthID, session.SrcNode)
if err != nil {
// Clean up the session. Doing this on any error from control
// server to avoid the user getting stuck with a bad session
// cookie.
s.browserSessions.Delete(session.ID)
return err
}
if a.Complete {
session.Authenticated = a.Complete
s.browserSessions.Store(session.ID, session)
}
return nil
}
func (s *Server) newSessionID() (string, error) {
raw := make([]byte, 16)
for i := 0; i < 5; i++ {
if _, err := rand.Read(raw); err != nil {
return "", err
}
cookie := "ts-web-" + base64.RawURLEncoding.EncodeToString(raw)
if _, ok := s.browserSessions.Load(cookie); !ok {
return cookie, nil
}
}
return "", errors.New("too many collisions generating new session; please refresh page")
}
// peerCapabilities holds information about what a source
// peer is allowed to edit via the web UI.
//
// map value is true if the peer can edit the given feature.
// Only capFeatures included in validCaps will be included.
type peerCapabilities map[capFeature]bool
// canEdit is true if the peerCapabilities grant edit access
// to the given feature.
func (p peerCapabilities) canEdit(feature capFeature) bool {
if p == nil {
return false
}
if p[capFeatureAll] {
return true
}
return p[feature]
}
// isEmpty is true if p is either nil or has no capabilities
// with value true.
func (p peerCapabilities) isEmpty() bool {
if p == nil {
return true
}
for _, v := range p {
if v == true {
return false
}
}
return true
}
type capFeature string
const (
// The following values should not be edited.
// New caps can be added, but existing ones should not be changed,
// as these exact values are used by users in tailnet policy files.
//
// IMPORTANT: When adding a new cap, also update validCaps slice below.
capFeatureAll capFeature = "*" // grants peer management of all features
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnets capFeature = "subnets" // grants peer subnet routes management
capFeatureExitNodes capFeature = "exitnodes" // grants peer ability to advertise-as and use exit nodes
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
)
// validCaps contains the list of valid capabilities used in the web client.
// Any capabilities included in a peer's grants that do not fall into this
// list will be ignored.
var validCaps []capFeature = []capFeature{
capFeatureAll,
capFeatureSSH,
capFeatureSubnets,
capFeatureExitNodes,
capFeatureAccount,
}
type capRule struct {
CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
}
// toPeerCapabilities parses out the web ui capabilities from the
// given whois response.
func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) (peerCapabilities, error) {
if whois == nil || status == nil {
return peerCapabilities{}, nil
}
if whois.Node.IsTagged() {
// We don't allow management *from* tagged nodes, so ignore caps.
// The web client auth flow relies on having a true user identity
// that can be verified through login.
return peerCapabilities{}, nil
}
if !status.Self.IsTagged() {
// User owned nodes are only ever manageable by the owner.
if status.Self.UserID != whois.UserProfile.ID {
return peerCapabilities{}, nil
} else {
return peerCapabilities{capFeatureAll: true}, nil // owner can edit all features
}
}
// For tagged nodes, we actually look at the granted capabilities.
caps := peerCapabilities{}
rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
}
for _, c := range rules {
for _, f := range c.CanEdit {
cap := capFeature(strings.ToLower(f))
if slices.Contains(validCaps, cap) {
caps[cap] = true
}
}
}
return caps, nil
}

View File

@@ -6,10 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<script type="module" crossorigin src="./assets/index-4d1f45ea.js"></script>
<link rel="stylesheet" href="./assets/index-8612dca6.css">
<link rel="preload" as="font" href="./assets/Inter.var.latin-39e72c07.woff2" type="font/woff2" crossorigin />
<script type="module" crossorigin src="./assets/index-fd4af382.js"></script>
<link rel="stylesheet" href="./assets/index-218918fa.css">
</head>
<body>
<body class="px-2">
<noscript>
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>

View File

@@ -6,21 +6,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<link rel="stylesheet" type="text/css" href="/src/index.css" />
<link rel="preload" as="font" href="/src/assets/fonts/Inter.var.latin.woff2" type="font/woff2" crossorigin />
</head>
<body>
<body class="px-2">
<noscript>
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
</noscript>
<script type="module" src="/src/index.tsx"></script>
<script>
// if this script is changed, also change hash in web.go
window.addEventListener("load", () => {
if (!window.Tailscale) {
const rootEl = document.createElement("p")
rootEl.innerHTML = 'Tailscale was built without the web client. See <a href="https://github.com/tailscale/tailscale#building-the-web-client">Building the web client</a> for more information.'
rootEl.innerHTML = 'Tailscale web interface is unavailable.';
document.body.append(rootEl)
}
});
})
</script>
</body>
</html>

View File

@@ -6,39 +6,76 @@
"node": "18.16.1",
"yarn": "1.22.19"
},
"type": "module",
"private": true,
"dependencies": {
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.6",
"classnames": "^2.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"swr": "^2.2.4",
"wouter": "^2.11.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@types/classnames": "^2.2.10",
"@types/node": "^18.16.1",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react-swc": "^3.3.2",
"@vitejs/plugin-react-swc": "^3.6.0",
"autoprefixer": "^10.4.15",
"postcss": "^8.4.27",
"eslint": "^8.23.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-curly-quotes": "^1.0.4",
"jsdom": "^23.0.1",
"postcss": "^8.4.31",
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2",
"tailwindcss": "^3.3.3",
"typescript": "^4.7.4",
"vite": "^4.3.9",
"vite-plugin-rewrite-all": "^1.0.1",
"vite-plugin-svgr": "^3.2.0",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.32.0"
"vitest": "^1.3.1"
},
"resolutions": {
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1"
},
"scripts": {
"build": "vite build",
"start": "vite",
"lint": "tsc --noEmit",
"lint": "tsc --noEmit && eslint 'src/**/*.{ts,tsx,js,jsx}'",
"test": "vitest",
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"format-check": "prettier --check 'src/**/*.{ts,tsx}'"
},
"eslintConfig": {
"extends": [
"react-app"
],
"plugins": [
"curly-quotes",
"react-hooks"
],
"rules": {
"curly-quotes/no-straight-quotes": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error"
},
"settings": {
"projectRoot": "client/web/package.json"
}
},
"prettier": {
"semi": false,
"printWidth": 80
},
"postcss": {
"plugins": {
"tailwindcss": {},
"autoprefixer": {}
}
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -9,6 +9,7 @@ package web
import (
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
@@ -18,21 +19,17 @@ import (
// authorizeQNAP authenticates the logged-in QNAP user and verifies that they
// are authorized to use the web client.
// It reports true if the request is authorized to continue, and false otherwise.
// authorizeQNAP manages writing out any relevant authorization errors to the
// ResponseWriter itself.
func authorizeQNAP(w http.ResponseWriter, r *http.Request) (ok bool) {
// If the user is not authorized to use the client, an error is returned.
func authorizeQNAP(r *http.Request) (authorized bool, err error) {
_, resp, err := qnapAuthn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return false
return false, err
}
if resp.IsAdmin == 0 {
http.Error(w, "user is not an admin", http.StatusForbidden)
return false
return false, errors.New("user is not an admin")
}
return true
return true, nil
}
type qnapAuthResponse struct {

View File

@@ -1,23 +1,280 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useCallback } from "react"
import useToaster from "src/hooks/toaster"
import { ExitNode, NodeData, SubnetRoute } from "src/types"
import { assertNever } from "src/utils/util"
import { MutatorOptions, SWRConfiguration, useSWRConfig } from "swr"
import { noExitNode, runAsExitNode } from "./hooks/exit-nodes"
export const swrConfig: SWRConfiguration = {
fetcher: (url: string) => apiFetch(url, "GET"),
onError: (err, _) => console.error(err),
}
type APIType =
| { action: "up"; data: TailscaleUpData }
| { action: "logout" }
| { action: "new-auth-session"; data: AuthSessionNewData }
| { action: "update-prefs"; data: LocalPrefsData }
| { action: "update-routes"; data: SubnetRoute[] }
| { action: "update-exit-node"; data: ExitNode }
/**
* POST /api/up data
*/
type TailscaleUpData = {
Reauthenticate?: boolean // force reauthentication
ControlURL?: string
AuthKey?: string
}
/**
* GET /api/auth/session/new data
*/
type AuthSessionNewData = {
authUrl: string
}
/**
* PATCH /api/local/v0/prefs data
*/
type LocalPrefsData = {
RunSSHSet?: boolean
RunSSH?: boolean
}
/**
* POST /api/routes data
*/
type RoutesData = {
SetExitNode?: boolean
SetRoutes?: boolean
UseExitNode?: string
AdvertiseExitNode?: boolean
AdvertiseRoutes?: string[]
}
/**
* useAPI hook returns an api handler that can execute api calls
* throughout the web client UI.
*/
export function useAPI() {
const toaster = useToaster()
const { mutate } = useSWRConfig() // allows for global mutation
const handlePostError = useCallback(
(toast?: string) => (err: Error) => {
console.error(err)
toast && toaster.show({ variant: "danger", message: toast })
throw err
},
[toaster]
)
/**
* optimisticMutate wraps the SWR `mutate` function to apply some
* type-awareness with the following behavior:
*
* 1. `optimisticData` update is applied immediately on FetchDataType
* throughout the web client UI.
*
* 2. `fetch` data mutation runs.
*
* 3. On completion, FetchDataType is revalidated to exactly reflect the
* updated server state.
*
* The `key` argument is the useSWR key associated with the MutateDataType.
* All `useSWR(key)` consumers throughout the UI will see updates reflected.
*/
const optimisticMutate = useCallback(
<MutateDataType, FetchDataType = any>(
key: string,
fetch: Promise<FetchDataType>,
optimisticData: (current: MutateDataType) => MutateDataType,
revalidate?: boolean // optionally specify whether to run final revalidation (step 3)
): Promise<FetchDataType | undefined> => {
const options: MutatorOptions = {
/**
* populateCache is meant for use when the remote request returns back
* the updated data directly. i.e. When FetchDataType is the same as
* MutateDataType. Most of our data manipulation requests return a 200
* with empty data on success. We turn off populateCache so that the
* cache only gets updated after completion of the remote reqeust when
* the revalidation step runs.
*/
populateCache: false,
optimisticData,
revalidate: revalidate,
}
return mutate(key, fetch, options)
},
[mutate]
)
const api = useCallback(
(t: APIType) => {
switch (t.action) {
/**
* "up" handles authenticating the machine to tailnet.
*/
case "up":
return apiFetch<{ url?: string }>("/up", "POST", t.data)
.then((d) => d.url && window.open(d.url, "_blank")) // "up" login step
.then(() => incrementMetric("web_client_node_connect"))
.then(() => mutate("/data"))
.catch(handlePostError("Failed to login"))
/**
* "logout" handles logging the node out of tailscale, effectively
* expiring its node key.
*/
case "logout":
// For logout, must increment metric before running api call,
// as tailscaled will be unreachable after the call completes.
incrementMetric("web_client_node_disconnect")
return apiFetch("/local/v0/logout", "POST").catch(
handlePostError("Failed to logout")
)
/**
* "new-auth-session" handles creating a new check mode session to
* authorize the viewing user to manage the node via the web client.
*/
case "new-auth-session":
return apiFetch<AuthSessionNewData>("/auth/session/new", "GET").catch(
handlePostError("Failed to create new session")
)
/**
* "update-prefs" handles setting the node's tailscale prefs.
*/
case "update-prefs": {
return optimisticMutate<NodeData>(
"/data",
apiFetch<LocalPrefsData>("/local/v0/prefs", "PATCH", t.data),
(old) => ({
...old,
RunningSSHServer: t.data.RunSSHSet
? Boolean(t.data.RunSSH)
: old.RunningSSHServer,
})
)
.then(
() =>
t.data.RunSSHSet &&
incrementMetric(
t.data.RunSSH
? "web_client_ssh_enable"
: "web_client_ssh_disable"
)
)
.catch(handlePostError("Failed to update node preference"))
}
/**
* "update-routes" handles setting the node's advertised routes.
*/
case "update-routes": {
const body: RoutesData = {
SetRoutes: true,
AdvertiseRoutes: t.data.map((r) => r.Route),
}
return optimisticMutate<NodeData>(
"/data",
apiFetch<void>("/routes", "POST", body),
(old) => ({ ...old, AdvertisedRoutes: t.data })
)
.then(() => incrementMetric("web_client_advertise_routes_change"))
.catch(handlePostError("Failed to update routes"))
}
/**
* "update-exit-node" handles updating the node's state as either
* running as an exit node or using another node as an exit node.
*/
case "update-exit-node": {
const id = t.data.ID
const body: RoutesData = {
SetExitNode: true,
}
if (id !== noExitNode.ID && id !== runAsExitNode.ID) {
body.UseExitNode = id
} else if (id === runAsExitNode.ID) {
body.AdvertiseExitNode = true
}
const metrics: MetricName[] = []
return optimisticMutate<NodeData>(
"/data",
apiFetch<void>("/routes", "POST", body),
(old) => {
// Only update metrics whose values have changed.
if (old.AdvertisingExitNode !== Boolean(body.AdvertiseExitNode)) {
metrics.push(
body.AdvertiseExitNode
? "web_client_advertise_exitnode_enable"
: "web_client_advertise_exitnode_disable"
)
}
if (Boolean(old.UsingExitNode) !== Boolean(body.UseExitNode)) {
metrics.push(
body.UseExitNode
? "web_client_use_exitnode_enable"
: "web_client_use_exitnode_disable"
)
}
return {
...old,
UsingExitNode: Boolean(body.UseExitNode) ? t.data : undefined,
AdvertisingExitNode: Boolean(body.AdvertiseExitNode),
AdvertisingExitNodeApproved: Boolean(body.AdvertiseExitNode)
? true // gets updated in revalidation
: old.AdvertisingExitNodeApproved,
}
},
false // skip final revalidation
)
.then(() => metrics.forEach((m) => incrementMetric(m)))
.catch(handlePostError("Failed to update exit node"))
}
default:
assertNever(t)
}
},
[handlePostError, mutate, optimisticMutate]
)
return api
}
let csrfToken: string
let synoToken: string | undefined // required for synology API requests
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
// apiFetch wraps the standard JS fetch function with csrf header
// management and param additions specific to the web client.
//
// apiFetch adds the `api` prefix to the request URL,
// so endpoint should be provided without the `api` prefix
// (i.e. provide `/data` rather than `api/data`).
export function apiFetch(
/**
* apiFetch wraps the standard JS fetch function with csrf header
* management and param additions specific to the web client.
*
* apiFetch adds the `api` prefix to the request URL,
* so endpoint should be provided without the `api` prefix
* (i.e. provide `/data` rather than `api/data`).
*/
export function apiFetch<T>(
endpoint: string,
method: "GET" | "POST",
body?: any,
params?: Record<string, string>
): Promise<Response> {
method: "GET" | "POST" | "PATCH",
body?: any
): Promise<T> {
const urlParams = new URLSearchParams(window.location.search)
const nextParams = new URLSearchParams(params)
const token = urlParams.get("SynoToken")
if (token) {
nextParams.set("SynoToken", token)
const nextParams = new URLSearchParams()
if (synoToken) {
nextParams.set("SynoToken", synoToken)
} else {
const token = urlParams.get("SynoToken")
if (token) {
nextParams.set("SynoToken", token)
}
}
const search = nextParams.toString()
const url = `api${endpoint}${search ? `?${search}` : ""}`
@@ -43,16 +300,26 @@ export function apiFetch(
"Content-Type": contentType,
"X-CSRF-Token": csrfToken,
},
body,
}).then((r) => {
updateCsrfToken(r)
if (!r.ok) {
return r.text().then((err) => {
throw new Error(err)
})
}
return r
body: body,
})
.then((r) => {
updateCsrfToken(r)
if (!r.ok) {
return r.text().then((err) => {
throw new Error(err)
})
}
return r
})
.then((r) => {
if (r.headers.get("Content-Type") === "application/json") {
return r.json()
}
})
.then((r) => {
r?.UnraidToken && setUnraidCsrfToken(r.UnraidToken)
return r
})
}
function updateCsrfToken(r: Response) {
@@ -62,6 +329,49 @@ function updateCsrfToken(r: Response) {
}
}
export function setUnraidCsrfToken(token?: string) {
export function setSynoToken(token?: string) {
synoToken = token
}
function setUnraidCsrfToken(token?: string) {
unraidCsrfToken = token
}
/**
* incrementMetric hits the client metrics local API endpoint to
* increment the given counter metric by one.
*/
export function incrementMetric(metricName: MetricName) {
const postData: MetricsPOSTData[] = [
{
Name: metricName,
Type: "counter",
Value: 1,
},
]
apiFetch("/local/v0/upload-client-metrics", "POST", postData).catch(
(error) => {
console.error(error)
}
)
}
type MetricsPOSTData = {
Name: MetricName
Type: MetricType
Value: number
}
type MetricType = "counter" | "gauge"
export type MetricName =
| "web_client_advertise_exitnode_enable"
| "web_client_advertise_exitnode_disable"
| "web_client_use_exitnode_enable"
| "web_client_use_exitnode_disable"
| "web_client_ssh_enable"
| "web_client_ssh_disable"
| "web_client_node_connect"
| "web_client_node_disconnect"
| "web_client_advertise_routes_change"

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12.5H19" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5.5L19 12.5L12 19.5" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 12L12 8L8 12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 16V8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 11.08V12C21.9988 14.1564 21.3005 16.2547 20.0093 17.9818C18.7182 19.709 16.9033 20.9725 14.8354 21.5839C12.7674 22.1953 10.5573 22.1219 8.53447 21.3746C6.51168 20.6273 4.78465 19.2461 3.61096 17.4371C2.43727 15.628 1.87979 13.4881 2.02168 11.3363C2.16356 9.18455 2.99721 7.13631 4.39828 5.49706C5.79935 3.85781 7.69279 2.71537 9.79619 2.24013C11.8996 1.7649 14.1003 1.98232 16.07 2.85999" stroke="#1EA672" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 4L12 14.01L9 11.01" stroke="#1EA672" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6673 5L7.50065 14.1667L3.33398 10" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 7.5L10 12.5L15 7.5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_14876_118476)">
<path d="M8.00065 14.6667C11.6825 14.6667 14.6673 11.6819 14.6673 8.00004C14.6673 4.31814 11.6825 1.33337 8.00065 1.33337C4.31875 1.33337 1.33398 4.31814 1.33398 8.00004C1.33398 11.6819 4.31875 14.6667 8.00065 14.6667Z" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4V8L10.6667 9.33333" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_14876_118476">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#292828" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#292828" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,11 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_15367_14595)">
<path d="M0.625 8C0.625 8 3.125 3 7.5 3C11.875 3 14.375 8 14.375 8C14.375 8 11.875 13 7.5 13C3.125 13 0.625 8 0.625 8Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 9.875C8.53553 9.875 9.375 9.03553 9.375 8C9.375 6.96447 8.53553 6.125 7.5 6.125C6.46447 6.125 5.625 6.96447 5.625 8C5.625 9.03553 6.46447 9.875 7.5 9.875Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_15367_14595">
<rect width="15" height="15" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View File

@@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_14860_117136)">
<path d="M16.666 1.66667H3.33268C2.41221 1.66667 1.66602 2.41286 1.66602 3.33334V6.66667C1.66602 7.58715 2.41221 8.33334 3.33268 8.33334H16.666C17.5865 8.33334 18.3327 7.58715 18.3327 6.66667V3.33334C18.3327 2.41286 17.5865 1.66667 16.666 1.66667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.666 11.6667H3.33268C2.41221 11.6667 1.66602 12.4129 1.66602 13.3333V16.6667C1.66602 17.5871 2.41221 18.3333 3.33268 18.3333H16.666C17.5865 18.3333 18.3327 17.5871 18.3327 16.6667V13.3333C18.3327 12.4129 17.5865 11.6667 16.666 11.6667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 5H5.01" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H5.01" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_14860_117136">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4.16663V15.8333" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.16602 10H15.8327" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.16667 15.8333C12.8486 15.8333 15.8333 12.8486 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333Z" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.5 17.5L13.875 13.875" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 13.625V12.375C12.5 11.712 12.2366 11.0761 11.7678 10.6072C11.2989 10.1384 10.663 9.875 10 9.875H5C4.33696 9.875 3.70107 10.1384 3.23223 10.6072C2.76339 11.0761 2.5 11.712 2.5 12.375V13.625" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 7.375C8.88071 7.375 10 6.25571 10 4.875C10 3.49429 8.88071 2.375 7.5 2.375C6.11929 2.375 5 3.49429 5 4.875C5 6.25571 6.11929 7.375 7.5 7.375Z" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L9 15" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 6L18 18" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@@ -0,0 +1,28 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
import Badge from "src/ui/badge"
/**
* ACLTag handles the display of an ACL tag.
*/
export default function ACLTag({
tag,
className,
}: {
tag: string
className?: string
}) {
return (
<Badge
variant="status"
color="outline"
className={cx("flex text-xs items-center", className)}
>
<span className="font-medium">tag:</span>
<span className="text-gray-500">{tag.replace("tag:", "")}</span>
</Badge>
)
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import * as Primitive from "@radix-ui/react-popover"
import cx from "classnames"
import React, { useCallback } from "react"
import ChevronDown from "src/assets/icons/chevron-down.svg?react"
import Copy from "src/assets/icons/copy.svg?react"
import NiceIP from "src/components/nice-ip"
import useToaster from "src/hooks/toaster"
import Button from "src/ui/button"
import { copyText } from "src/utils/clipboard"
/**
* AddressCard renders a clickable IP address text that opens a
* dialog with a copyable list of all addresses (IPv4, IPv6, DNS)
* for the machine.
*/
export default function AddressCard({
v4Address,
v6Address,
shortDomain,
fullDomain,
className,
triggerClassName,
}: {
v4Address: string
v6Address: string
shortDomain?: string
fullDomain?: string
className?: string
triggerClassName?: string
}) {
const children = (
<ul className="flex flex-col divide-y rounded-md overflow-hidden">
{shortDomain && <AddressRow label="short domain" value={shortDomain} />}
{fullDomain && <AddressRow label="full domain" value={fullDomain} />}
{v4Address && (
<AddressRow
key={v4Address}
label="IPv4 address"
ip={true}
value={v4Address}
/>
)}
{v6Address && (
<AddressRow
key={v6Address}
label="IPv6 address"
ip={true}
value={v6Address}
/>
)}
</ul>
)
return (
<Primitive.Root>
<Primitive.Trigger asChild>
<Button
variant="minimal"
className={cx("-ml-1 px-1 py-0 font-normal", className)}
suffixIcon={
<ChevronDown className="w-5 h-5" stroke="#232222" /* gray-800 */ />
}
aria-label="See all addresses for this device."
>
<NiceIP className={triggerClassName} ip={v4Address ?? v6Address} />
</Button>
</Primitive.Trigger>
<Primitive.Content
className="shadow-popover origin-radix-popover state-open:animate-scale-in state-closed:animate-scale-out bg-white rounded-md z-50 max-w-sm"
sideOffset={10}
side="top"
>
{children}
</Primitive.Content>
</Primitive.Root>
)
}
function AddressRow({
label,
value,
ip,
}: {
label: string
value: string
ip?: boolean
}) {
const toaster = useToaster()
const onCopyClick = useCallback(() => {
copyText(value)
.then(() => toaster.show({ message: `Copied ${label} to clipboard` }))
.catch(() =>
toaster.show({
message: `Failed to copy ${label} to clipboard`,
variant: "danger",
})
)
}, [label, toaster, value])
return (
<li className="py flex items-center gap-2">
<button
className={cx(
"relative flex group items-center transition-colors",
"focus:outline-none focus-visible:ring",
"disabled:text-text-muted enabled:hover:text-gray-500",
"w-60 text-sm flex-1"
)}
onClick={onCopyClick}
aria-label={`Copy ${value} to your clip board.`}
>
<div className="overflow-hidden pl-3 pr-10 py-2 tabular-nums">
{ip ? (
<NiceIP ip={value} />
) : (
<div className="truncate m-w-full">{value}</div>
)}
</div>
<span
className={cx(
"absolute right-0 pl-6 pr-3 bg-gradient-to-r from-transparent",
"text-gray-900 group-hover:text-gray-600"
)}
>
<Copy className="w-4 h-4" />
</span>
</button>
</li>
)
}

View File

@@ -1,124 +1,172 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import { Footer, Header, IP, State } from "src/components/legacy"
import useNodeData, { NodeData } from "src/hooks/node-data"
import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"
import TailscaleIcon from "src/assets/icons/tailscale-icon.svg?react"
import LoginToggle from "src/components/login-toggle"
import DeviceDetailsView from "src/components/views/device-details-view"
import DisconnectedView from "src/components/views/disconnected-view"
import HomeView from "src/components/views/home-view"
import LoginView from "src/components/views/login-view"
import SSHView from "src/components/views/ssh-view"
import SubnetRouterView from "src/components/views/subnet-router-view"
import { UpdatingView } from "src/components/views/updating-view"
import useAuth, { AuthResponse, canEdit } from "src/hooks/auth"
import { Feature, NodeData, featureDescription } from "src/types"
import Card from "src/ui/card"
import EmptyState from "src/ui/empty-state"
import LoadingDots from "src/ui/loading-dots"
import useSWR from "swr"
import { Link, Route, Router, Switch, useLocation } from "wouter"
export default function App() {
// TODO(sonia): use isPosting value from useNodeData
// to fill loading states.
const { data, refreshData, updateNode } = useNodeData()
const { data: auth, loading: loadingAuth, newSession } = useAuth()
if (!data) {
// TODO(sonia): add a loading view
return <div className="text-center py-14">Loading...</div>
}
const needsLogin = data?.Status === "NeedsLogin" || data?.Status === "NoState"
return !needsLogin &&
(data.DebugMode === "login" || data.DebugMode === "full") ? (
<div className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-10">
{data.DebugMode === "login" ? (
<LoginView {...data} />
return (
<main className="min-w-sm max-w-lg mx-auto py-4 sm:py-14 px-5">
{loadingAuth || !auth ? (
<LoadingView />
) : (
<ManageView {...data} />
<WebClient auth={auth} newSession={newSession} />
)}
<Footer className="mt-20" licensesURL={data.LicensesURL} />
</div>
) : (
// Legacy client UI
<div className="py-14">
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header data={data} refreshData={refreshData} updateNode={updateNode} />
<IP data={data} />
<State data={data} updateNode={updateNode} />
</main>
<Footer licensesURL={data.LicensesURL} />
</div>
</main>
)
}
function LoginView(props: NodeData) {
return (
function WebClient({
auth,
newSession,
}: {
auth: AuthResponse
newSession: () => Promise<void>
}) {
const { data: node } = useSWR<NodeData>("/data")
return !node ? (
<LoadingView />
) : node.Status === "NeedsLogin" ||
node.Status === "NoState" ||
node.Status === "Stopped" ? (
// Client not on a tailnet, render login.
<LoginView data={node} />
) : (
// Otherwise render the new web client.
<>
<div className="pb-52 mx-auto">
<TailscaleLogo />
</div>
<div className="w-full p-4 bg-stone-50 rounded-3xl border border-gray-200 flex flex-col gap-4">
<div className="flex gap-2.5">
<ProfilePic url={props.Profile.ProfilePicURL} />
<div className="font-medium">
<div className="text-neutral-500 text-xs uppercase tracking-wide">
Owned by
</div>
<div className="text-neutral-800 text-sm leading-tight">
{/* TODO(sonia): support tagged node profile view more eloquently */}
{props.Profile.LoginName}
</div>
</div>
</div>
<div className="px-5 py-4 bg-white rounded-lg border border-gray-200 justify-between items-center flex">
<div className="flex gap-3">
<ConnectedDeviceIcon />
<div className="text-neutral-800">
<div className="text-lg font-medium leading-[25.20px]">
{props.DeviceName}
</div>
<div className="text-sm leading-tight">{props.IP}</div>
</div>
</div>
<button className="button button-blue ml-6">Access</button>
</div>
</div>
<Router base={node.URLPrefix}>
<Header node={node} auth={auth} newSession={newSession} />
<Switch>
<Route path="/">
<HomeView node={node} auth={auth} />
</Route>
<Route path="/details">
<DeviceDetailsView node={node} auth={auth} />
</Route>
<FeatureRoute path="/subnets" feature="advertise-routes" node={node}>
<SubnetRouterView
readonly={!canEdit("subnets", auth)}
node={node}
/>
</FeatureRoute>
<FeatureRoute path="/ssh" feature="ssh" node={node}>
<SSHView readonly={!canEdit("ssh", auth)} node={node} />
</FeatureRoute>
{/* <Route path="/serve">Share local content</Route> */}
<FeatureRoute path="/update" feature="auto-update" node={node}>
<UpdatingView
versionInfo={node.ClientVersion}
currentVersion={node.IPNVersion}
/>
</FeatureRoute>
<Route path="/disconnected">
<DisconnectedView />
</Route>
<Route>
<Card className="mt-8">
<EmptyState description="Page not found" />
</Card>
</Route>
</Switch>
</Router>
</>
)
}
function ManageView(props: NodeData) {
/**
* FeatureRoute renders a Route component,
* but only displays the child view if the specified feature is
* available for use on this node's platform. If not available,
* a not allowed view is rendered instead.
*/
function FeatureRoute({
path,
node,
feature,
children,
}: {
path: string
node: NodeData
feature: Feature
children: React.ReactNode
}) {
return (
<div className="px-5">
<div className="flex justify-between mb-12">
<TailscaleIcon />
<div className="flex">
<p className="mr-2">{props.Profile.LoginName}</p>
{/* TODO(sonia): support tagged node profile view more eloquently */}
<ProfilePic url={props.Profile.ProfilePicURL} />
</div>
</div>
<p className="tracking-wide uppercase text-gray-600 pb-3">This device</p>
<div className="-mx-5 border rounded-md px-5 py-4 bg-white">
<div className="flex justify-between items-center text-lg">
<div className="flex items-center">
<ConnectedDeviceIcon />
<p className="font-medium ml-3">{props.DeviceName}</p>
</div>
<p className="tracking-widest">{props.IP}</p>
</div>
</div>
<p className="text-gray-500 pt-2">
Tailscale is up and running. You can connect to this device from devices
in your tailnet by using its name or IP address.
</p>
</div>
<Route path={path}>
{!node.Features[feature] ? (
<Card className="mt-8">
<EmptyState
description={`${featureDescription(
feature
)} not available on this device.`}
/>
</Card>
) : (
children
)}
</Route>
)
}
function ProfilePic({ url }: { url: string }) {
function Header({
node,
auth,
newSession,
}: {
node: NodeData
auth: AuthResponse
newSession: () => Promise<void>
}) {
const [loc] = useLocation()
if (loc === "/disconnected") {
// No header on view presented after logout.
return null
}
return (
<div className="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{url ? (
<div
className="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style={{
backgroundImage: `url(${url})`,
backgroundSize: "cover",
}}
/>
) : (
<div className="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
<>
<div className="flex flex-wrap gap-4 justify-between items-center mb-9 md:mb-12">
<Link to="/" className="flex gap-3 overflow-hidden">
<TailscaleIcon />
<div className="inline text-gray-800 text-lg font-medium leading-snug truncate">
{node.DomainName}
</div>
</Link>
<LoginToggle node={node} auth={auth} newSession={newSession} />
</div>
{loc !== "/" && loc !== "/update" && (
<Link to="/" className="link font-medium block mb-2">
&larr; Back to {node.DeviceName}
</Link>
)}
</div>
</>
)
}
/**
* LoadingView fills its container with small animated loading dots
* in the center.
*/
export function LoadingView() {
return (
<LoadingDots className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
)
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import { NodeData } from "src/types"
/**
* AdminContainer renders its contents only if the node's control
* server has an admin panel.
*
* TODO(sonia,will): Similarly, this could also hide the contents
* if the viewing user is a non-admin.
*/
export function AdminContainer({
node,
children,
className,
}: {
node: NodeData
children: React.ReactNode
className?: string
}) {
if (!node.ControlAdminURL.includes("tailscale.com")) {
// Admin panel only exists on Tailscale control servers.
return null
}
return <div className={className}>{children}</div>
}
/**
* AdminLink renders its contents wrapped in a link to the node's control
* server admin panel.
*
* AdminLink is meant for use only inside of a AdminContainer component,
* to avoid rendering a link when the node's control server does not have
* an admin panel.
*/
export function AdminLink({
node,
children,
path,
}: {
node: NodeData
children: React.ReactNode
path: string // admin path, e.g. "/settings/webhooks"
}) {
return (
<a
href={`${node.ControlAdminURL}${path}`}
className="link"
target="_blank"
rel="noreferrer"
>
{children}
</a>
)
}

View File

@@ -0,0 +1,584 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useAPI } from "src/api"
import Check from "src/assets/icons/check.svg?react"
import ChevronDown from "src/assets/icons/chevron-down.svg?react"
import useExitNodes, {
noExitNode,
runAsExitNode,
trimDNSSuffix,
} from "src/hooks/exit-nodes"
import { ExitNode, NodeData } from "src/types"
import Popover from "src/ui/popover"
import SearchInput from "src/ui/search-input"
import { useSWRConfig } from "swr"
export default function ExitNodeSelector({
className,
node,
disabled,
}: {
className?: string
node: NodeData
disabled?: boolean
}) {
const api = useAPI()
const [open, setOpen] = useState<boolean>(false)
const [selected, setSelected] = useState<ExitNode>(toSelectedExitNode(node))
const [pending, setPending] = useState<boolean>(false)
const { mutate } = useSWRConfig() // allows for global mutation
useEffect(() => setSelected(toSelectedExitNode(node)), [node])
useEffect(() => {
setPending(
node.AdvertisingExitNode && node.AdvertisingExitNodeApproved === false
)
}, [node])
const handleSelect = useCallback(
(n: ExitNode) => {
setOpen(false)
if (n.ID === selected.ID) {
return // no update
}
// Eager clear of pending state to avoid UI oddities
if (n.ID !== runAsExitNode.ID) {
setPending(false)
}
api({ action: "update-exit-node", data: n })
// refresh data after short timeout to pick up any pending approval updates
setTimeout(() => {
mutate("/data")
}, 1000)
},
[api, mutate, selected.ID]
)
const [
none, // not using exit nodes
advertising, // advertising as exit node
using, // using another exit node
offline, // selected exit node node is offline
] = useMemo(
() => [
selected.ID === noExitNode.ID,
selected.ID === runAsExitNode.ID,
selected.ID !== noExitNode.ID && selected.ID !== runAsExitNode.ID,
!selected.Online,
],
[selected.ID, selected.Online]
)
return (
<div
className={cx(
"rounded-md",
{
"bg-red-600": offline,
"bg-yellow-400": pending,
},
className
)}
>
<div
className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", {
"border-gray-200": none,
"bg-yellow-300 border-yellow-300": advertising && !offline,
"bg-blue-500 border-blue-500": using && !offline,
"bg-red-500 border-red-500": offline,
})}
>
<Popover
open={disabled ? false : open}
onOpenChange={setOpen}
className="overflow-hidden"
side="bottom"
sideOffset={0}
align="start"
content={
<ExitNodeSelectorInner
node={node}
selected={selected}
onSelect={handleSelect}
/>
}
asChild
>
<button
className={cx("flex-1 px-2 py-1.5 rounded-[1px]", {
"bg-white": none,
"hover:bg-gray-100": none && !disabled,
"bg-yellow-300": advertising && !offline,
"hover:bg-yellow-200": advertising && !offline && !disabled,
"bg-blue-500": using && !offline,
"hover:bg-blue-400": using && !offline && !disabled,
"bg-red-500": offline,
"hover:bg-red-400": offline && !disabled,
})}
onClick={() => setOpen(!open)}
disabled={disabled}
>
<p
className={cx(
"text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1",
{ "opacity-70 text-white": advertising || using }
)}
>
Exit node{offline && " offline"}
</p>
<div className="flex items-center">
<p
className={cx("text-gray-800", {
"text-white": advertising || using,
})}
>
{selected.Location && (
<>
<CountryFlag code={selected.Location.CountryCode} />{" "}
</>
)}
{selected === runAsExitNode
? "Running as exit node"
: selected.Name}
</p>
{!disabled && (
<ChevronDown
className={cx("ml-1", {
"stroke-gray-800": none,
"stroke-white": advertising || using,
})}
/>
)}
</div>
</button>
</Popover>
{!disabled && (advertising || using) && (
<button
className={cx("px-3 py-2 rounded-sm text-white", {
"hover:bg-yellow-200": advertising && !offline,
"hover:bg-blue-400": using && !offline,
"hover:bg-red-400": offline,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
>
Disable
</button>
)}
</div>
{offline && (
<p className="text-white p-3">
The selected exit node is currently offline. Your internet traffic is
blocked until you disable the exit node or select a different one.
</p>
)}
{pending && (
<p className="text-white p-3">
Pending approval to run as exit node. This device wont be usable as
an exit node until then.
</p>
)}
</div>
)
}
function toSelectedExitNode(data: NodeData): ExitNode {
if (data.AdvertisingExitNode) {
return runAsExitNode
}
if (data.UsingExitNode) {
// TODO(sonia): also use online status
const node = { ...data.UsingExitNode }
if (node.Location) {
// For mullvad nodes, use location as name.
node.Name = `${node.Location.Country}: ${node.Location.City}`
} else {
// Otherwise use node name w/o DNS suffix.
node.Name = trimDNSSuffix(node.Name, data.TailnetName)
}
return node
}
return noExitNode
}
function ExitNodeSelectorInner({
node,
selected,
onSelect,
}: {
node: NodeData
selected: ExitNode
onSelect: (node: ExitNode) => void
}) {
const [filter, setFilter] = useState<string>("")
const { data: exitNodes } = useExitNodes(node, filter)
const listRef = useRef<HTMLDivElement>(null)
const hasNodes = useMemo(
() => exitNodes.find((n) => n.nodes.length > 0),
[exitNodes]
)
return (
<div className="w-[var(--radix-popover-trigger-width)]">
<SearchInput
name="exit-node-search"
className="px-2"
inputClassName="w-full py-3 !h-auto border-none rounded-b-none !ring-0"
autoFocus
autoCorrect="off"
autoComplete="off"
autoCapitalize="off"
placeholder="Search exit nodes…"
value={filter}
onChange={(e) => {
// Jump list to top when search value changes.
listRef.current?.scrollTo(0, 0)
setFilter(e.target.value)
}}
/>
{/* TODO(sonia): use loading spinner when loading useExitNodes */}
<div
ref={listRef}
className="pt-1 border-t border-gray-200 max-h-60 overflow-y-scroll"
>
{hasNodes ? (
exitNodes.map(
(group) =>
group.nodes.length > 0 && (
<div
key={group.id}
className="pb-1 mb-1 border-b last:border-b-0 border-gray-200 last:mb-0"
>
{group.name && (
<div className="px-4 py-2 text-gray-500 text-xs font-medium uppercase tracking-wide">
{group.name}
</div>
)}
{group.nodes.map((n) => (
<ExitNodeSelectorItem
key={`${n.ID}-${n.Name}`}
node={n}
onSelect={() => onSelect(n)}
isSelected={selected.ID === n.ID}
/>
))}
</div>
)
)
) : (
<div className="text-center truncate text-gray-500 p-5">
{filter
? `No exit nodes matching “${filter}`
: "No exit nodes available"}
</div>
)}
</div>
</div>
)
}
function ExitNodeSelectorItem({
node,
isSelected,
onSelect,
}: {
node: ExitNode
isSelected: boolean
onSelect: () => void
}) {
return (
<button
key={node.ID}
className={cx(
"w-full px-4 py-2 flex justify-between items-center cursor-pointer hover:bg-gray-100",
{
"text-gray-400 cursor-not-allowed": !node.Online,
}
)}
onClick={onSelect}
disabled={!node.Online}
>
<div className="w-full">
{node.Location && (
<>
<CountryFlag code={node.Location.CountryCode} />{" "}
</>
)}
<span className="leading-snug">{node.Name}</span>
</div>
{node.Online || <span className="leading-snug">Offline</span>}
{isSelected && <Check className="ml-1" />}
</button>
)
}
function CountryFlag({ code }: { code: string }) {
return (
<>{countryFlags[code.toLowerCase()]}</> || (
<span className="font-medium text-gray-500 text-xs">
{code.toUpperCase()}
</span>
)
)
}
const countryFlags: { [countryCode: string]: string } = {
ad: "🇦🇩",
ae: "🇦🇪",
af: "🇦🇫",
ag: "🇦🇬",
ai: "🇦🇮",
al: "🇦🇱",
am: "🇦🇲",
ao: "🇦🇴",
aq: "🇦🇶",
ar: "🇦🇷",
as: "🇦🇸",
at: "🇦🇹",
au: "🇦🇺",
aw: "🇦🇼",
ax: "🇦🇽",
az: "🇦🇿",
ba: "🇧🇦",
bb: "🇧🇧",
bd: "🇧🇩",
be: "🇧🇪",
bf: "🇧🇫",
bg: "🇧🇬",
bh: "🇧🇭",
bi: "🇧🇮",
bj: "🇧🇯",
bl: "🇧🇱",
bm: "🇧🇲",
bn: "🇧🇳",
bo: "🇧🇴",
bq: "🇧🇶",
br: "🇧🇷",
bs: "🇧🇸",
bt: "🇧🇹",
bv: "🇧🇻",
bw: "🇧🇼",
by: "🇧🇾",
bz: "🇧🇿",
ca: "🇨🇦",
cc: "🇨🇨",
cd: "🇨🇩",
cf: "🇨🇫",
cg: "🇨🇬",
ch: "🇨🇭",
ci: "🇨🇮",
ck: "🇨🇰",
cl: "🇨🇱",
cm: "🇨🇲",
cn: "🇨🇳",
co: "🇨🇴",
cr: "🇨🇷",
cu: "🇨🇺",
cv: "🇨🇻",
cw: "🇨🇼",
cx: "🇨🇽",
cy: "🇨🇾",
cz: "🇨🇿",
de: "🇩🇪",
dj: "🇩🇯",
dk: "🇩🇰",
dm: "🇩🇲",
do: "🇩🇴",
dz: "🇩🇿",
ec: "🇪🇨",
ee: "🇪🇪",
eg: "🇪🇬",
eh: "🇪🇭",
er: "🇪🇷",
es: "🇪🇸",
et: "🇪🇹",
eu: "🇪🇺",
fi: "🇫🇮",
fj: "🇫🇯",
fk: "🇫🇰",
fm: "🇫🇲",
fo: "🇫🇴",
fr: "🇫🇷",
ga: "🇬🇦",
gb: "🇬🇧",
gd: "🇬🇩",
ge: "🇬🇪",
gf: "🇬🇫",
gg: "🇬🇬",
gh: "🇬🇭",
gi: "🇬🇮",
gl: "🇬🇱",
gm: "🇬🇲",
gn: "🇬🇳",
gp: "🇬🇵",
gq: "🇬🇶",
gr: "🇬🇷",
gs: "🇬🇸",
gt: "🇬🇹",
gu: "🇬🇺",
gw: "🇬🇼",
gy: "🇬🇾",
hk: "🇭🇰",
hm: "🇭🇲",
hn: "🇭🇳",
hr: "🇭🇷",
ht: "🇭🇹",
hu: "🇭🇺",
id: "🇮🇩",
ie: "🇮🇪",
il: "🇮🇱",
im: "🇮🇲",
in: "🇮🇳",
io: "🇮🇴",
iq: "🇮🇶",
ir: "🇮🇷",
is: "🇮🇸",
it: "🇮🇹",
je: "🇯🇪",
jm: "🇯🇲",
jo: "🇯🇴",
jp: "🇯🇵",
ke: "🇰🇪",
kg: "🇰🇬",
kh: "🇰🇭",
ki: "🇰🇮",
km: "🇰🇲",
kn: "🇰🇳",
kp: "🇰🇵",
kr: "🇰🇷",
kw: "🇰🇼",
ky: "🇰🇾",
kz: "🇰🇿",
la: "🇱🇦",
lb: "🇱🇧",
lc: "🇱🇨",
li: "🇱🇮",
lk: "🇱🇰",
lr: "🇱🇷",
ls: "🇱🇸",
lt: "🇱🇹",
lu: "🇱🇺",
lv: "🇱🇻",
ly: "🇱🇾",
ma: "🇲🇦",
mc: "🇲🇨",
md: "🇲🇩",
me: "🇲🇪",
mf: "🇲🇫",
mg: "🇲🇬",
mh: "🇲🇭",
mk: "🇲🇰",
ml: "🇲🇱",
mm: "🇲🇲",
mn: "🇲🇳",
mo: "🇲🇴",
mp: "🇲🇵",
mq: "🇲🇶",
mr: "🇲🇷",
ms: "🇲🇸",
mt: "🇲🇹",
mu: "🇲🇺",
mv: "🇲🇻",
mw: "🇲🇼",
mx: "🇲🇽",
my: "🇲🇾",
mz: "🇲🇿",
na: "🇳🇦",
nc: "🇳🇨",
ne: "🇳🇪",
nf: "🇳🇫",
ng: "🇳🇬",
ni: "🇳🇮",
nl: "🇳🇱",
no: "🇳🇴",
np: "🇳🇵",
nr: "🇳🇷",
nu: "🇳🇺",
nz: "🇳🇿",
om: "🇴🇲",
pa: "🇵🇦",
pe: "🇵🇪",
pf: "🇵🇫",
pg: "🇵🇬",
ph: "🇵🇭",
pk: "🇵🇰",
pl: "🇵🇱",
pm: "🇵🇲",
pn: "🇵🇳",
pr: "🇵🇷",
ps: "🇵🇸",
pt: "🇵🇹",
pw: "🇵🇼",
py: "🇵🇾",
qa: "🇶🇦",
re: "🇷🇪",
ro: "🇷🇴",
rs: "🇷🇸",
ru: "🇷🇺",
rw: "🇷🇼",
sa: "🇸🇦",
sb: "🇸🇧",
sc: "🇸🇨",
sd: "🇸🇩",
se: "🇸🇪",
sg: "🇸🇬",
sh: "🇸🇭",
si: "🇸🇮",
sj: "🇸🇯",
sk: "🇸🇰",
sl: "🇸🇱",
sm: "🇸🇲",
sn: "🇸🇳",
so: "🇸🇴",
sr: "🇸🇷",
ss: "🇸🇸",
st: "🇸🇹",
sv: "🇸🇻",
sx: "🇸🇽",
sy: "🇸🇾",
sz: "🇸🇿",
tc: "🇹🇨",
td: "🇹🇩",
tf: "🇹🇫",
tg: "🇹🇬",
th: "🇹🇭",
tj: "🇹🇯",
tk: "🇹🇰",
tl: "🇹🇱",
tm: "🇹🇲",
tn: "🇹🇳",
to: "🇹🇴",
tr: "🇹🇷",
tt: "🇹🇹",
tv: "🇹🇻",
tw: "🇹🇼",
tz: "🇹🇿",
ua: "🇺🇦",
ug: "🇺🇬",
um: "🇺🇲",
us: "🇺🇸",
uy: "🇺🇾",
uz: "🇺🇿",
va: "🇻🇦",
vc: "🇻🇨",
ve: "🇻🇪",
vg: "🇻🇬",
vi: "🇻🇮",
vn: "🇻🇳",
vu: "🇻🇺",
wf: "🇼🇫",
ws: "🇼🇸",
xk: "🇽🇰",
ye: "🇾🇪",
yt: "🇾🇹",
za: "🇿🇦",
zm: "🇿🇲",
zw: "🇿🇼",
}

View File

@@ -1,298 +0,0 @@
import cx from "classnames"
import React from "react"
import { apiFetch } from "src/api"
import { NodeData, NodeUpdate } from "src/hooks/node-data"
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
// that (crudely) implement the pre-2023 web client. These are implemented
// purely to ease migration to the new React-based web client, and will
// eventually be completely removed.
export function Header({
data,
refreshData,
updateNode,
}: {
data: NodeData
refreshData: () => void
updateNode: (update: NodeUpdate) => void
}) {
return (
<header className="flex justify-between items-center min-width-0 py-2 mb-8">
<svg
width="26"
height="26"
viewBox="0 0 23 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="flex-shrink-0 mr-4"
>
<circle
opacity="0.2"
cx="3.4"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="3.4"
cy="19.5"
r="2.7"
fill="currentColor"
></circle>
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="11.5"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle
opacity="0.2"
cx="19.5"
cy="3.25"
r="2.7"
fill="currentColor"
></circle>
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
<circle
opacity="0.2"
cx="19.5"
cy="19.5"
r="2.7"
fill="currentColor"
></circle>
</svg>
<div className="flex items-center justify-end space-x-2 w-2/3">
{data.Profile &&
data.Status !== "NoState" &&
data.Status !== "NeedsLogin" && (
<>
<div className="text-right w-full leading-4">
<h4 className="truncate leading-normal">
{data.Profile.LoginName}
</h4>
<div className="text-xs text-gray-500 text-right">
<button
onClick={() => updateNode({ Reauthenticate: true })}
className="hover:text-gray-700"
>
Switch account
</button>{" "}
|{" "}
<button
onClick={() => updateNode({ Reauthenticate: true })}
className="hover:text-gray-700"
>
Reauthenticate
</button>{" "}
|{" "}
<button
onClick={() =>
apiFetch("/local/v0/logout", "POST")
.then(refreshData)
.catch((err) => alert("Logout failed: " + err.message))
}
className="hover:text-gray-700"
>
Logout
</button>
</div>
</div>
<div className="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{data.Profile.ProfilePicURL ? (
<div
className="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style={{
backgroundImage: `url(${data.Profile.ProfilePicURL})`,
backgroundSize: "cover",
}}
/>
) : (
<div className="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
)}
</div>
</>
)}
</div>
</header>
)
}
export function IP(props: { data: NodeData }) {
const { data } = props
if (!data.IP) {
return null
}
return (
<>
<div className="border border-gray-200 bg-gray-50 rounded-md p-2 pl-3 pr-3 width-full flex items-center justify-between">
<div className="flex items-center min-width-0">
<svg
className="flex-shrink-0 text-gray-600 mr-3 ml-1"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 className="font-semibold truncate mr-2">
{data.DeviceName || "Your device"}
</h4>
</div>
<h5>{data.IP}</h5>
</div>
<p className="mt-1 ml-1 mb-6 text-xs text-gray-600">
Debug info: Tailscale {data.IPNVersion}, tun={data.TUNMode.toString()}
{data.IsSynology && (
<>
, DSM{data.DSMVersion}
{data.TUNMode || (
<>
{" "}
(
<a
href="https://tailscale.com/kb/1152/synology-outbound/"
className="link-underline text-gray-600"
target="_blank"
aria-label="Configure outbound synology traffic"
rel="noopener noreferrer"
>
outgoing access not configured
</a>
)
</>
)}
</>
)}
</p>
</>
)
}
export function State({
data,
updateNode,
}: {
data: NodeData
updateNode: (update: NodeUpdate) => void
}) {
switch (data.Status) {
case "NeedsLogin":
case "NoState":
if (data.IP) {
return (
<>
<div className="mb-6">
<p className="text-gray-700">
Your device's key has expired. Reauthenticate this device by
logging in again, or{" "}
<a
href="https://tailscale.com/kb/1028/key-expiry"
className="link"
target="_blank"
>
learn more
</a>
.
</p>
</div>
<button
onClick={() => updateNode({ Reauthenticate: true })}
className="button button-blue w-full mb-4"
>
Reauthenticate
</button>
</>
)
} else {
return (
<>
<div className="mb-6">
<h3 className="text-3xl font-semibold mb-3">Log in</h3>
<p className="text-gray-700">
Get started by logging in to your Tailscale network.
Or,&nbsp;learn&nbsp;more at{" "}
<a
href="https://tailscale.com/"
className="link"
target="_blank"
>
tailscale.com
</a>
.
</p>
</div>
<button
onClick={() => updateNode({ Reauthenticate: true })}
className="button button-blue w-full mb-4"
>
Log In
</button>
</>
)
}
case "NeedsMachineAuth":
return (
<div className="mb-4">
This device is authorized, but needs approval from a network admin
before it can connect to the network.
</div>
)
default:
return (
<>
<div className="mb-4">
<p>
You are connected! Access this device over Tailscale using the
device name or IP address above.
</p>
</div>
<button
className={cx("button button-medium mb-4", {
"button-red": data.AdvertiseExitNode,
"button-blue": !data.AdvertiseExitNode,
})}
id="enabled"
onClick={() =>
updateNode({ AdvertiseExitNode: !data.AdvertiseExitNode })
}
>
{data.AdvertiseExitNode
? "Stop advertising Exit Node"
: "Advertise as Exit Node"}
</button>
</>
)
}
}
export function Footer(props: { licensesURL: string; className?: string }) {
return (
<footer
className={cx("container max-w-lg mx-auto text-center", props.className)}
>
<a
className="text-xs text-gray-500 hover:text-gray-600"
href={props.licensesURL}
>
Open Source Licenses
</a>
</footer>
)
}

View File

@@ -0,0 +1,376 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useCallback, useMemo, useState } from "react"
import ChevronDown from "src/assets/icons/chevron-down.svg?react"
import Eye from "src/assets/icons/eye.svg?react"
import User from "src/assets/icons/user.svg?react"
import { AuthResponse, hasAnyEditCapabilities } from "src/hooks/auth"
import { useTSWebConnected } from "src/hooks/ts-web-connected"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Popover from "src/ui/popover"
import ProfilePic from "src/ui/profile-pic"
import { assertNever, isHTTPS } from "src/utils/util"
export default function LoginToggle({
node,
auth,
newSession,
}: {
node: NodeData
auth: AuthResponse
newSession: () => Promise<void>
}) {
const [open, setOpen] = useState<boolean>(false)
const { tsWebConnected, checkTSWebConnection } = useTSWebConnected(
auth.serverMode,
node.IPv4
)
return (
<Popover
className="p-3 bg-white rounded-lg shadow flex flex-col max-w-[317px]"
content={
auth.serverMode === "readonly" ? (
<ReadonlyModeContent auth={auth} />
) : auth.serverMode === "login" ? (
<LoginModeContent
auth={auth}
node={node}
tsWebConnected={tsWebConnected}
checkTSWebConnection={checkTSWebConnection}
/>
) : auth.serverMode === "manage" ? (
<ManageModeContent auth={auth} node={node} newSession={newSession} />
) : (
assertNever(auth.serverMode)
)
}
side="bottom"
align="end"
open={open}
onOpenChange={setOpen}
asChild
>
<div>
{auth.authorized ? (
<TriggerWhenManaging auth={auth} open={open} setOpen={setOpen} />
) : (
<TriggerWhenReading auth={auth} open={open} setOpen={setOpen} />
)}
</div>
</Popover>
)
}
/**
* TriggerWhenManaging is displayed as the trigger for the login popover
* when the user has an active authorized managment session.
*/
function TriggerWhenManaging({
auth,
open,
setOpen,
}: {
auth: AuthResponse
open: boolean
setOpen: (next: boolean) => void
}) {
return (
<div
className={cx(
"w-[34px] h-[34px] p-1 rounded-full justify-center items-center inline-flex hover:bg-gray-300",
{
"bg-transparent": !open,
"bg-gray-300": open,
}
)}
>
<button onClick={() => setOpen(!open)}>
<ProfilePic size="medium" url={auth.viewerIdentity?.profilePicUrl} />
</button>
</div>
)
}
/**
* TriggerWhenReading is displayed as the trigger for the login popover
* when the user is currently in read mode (doesn't have an authorized
* management session).
*/
function TriggerWhenReading({
auth,
open,
setOpen,
}: {
auth: AuthResponse
open: boolean
setOpen: (next: boolean) => void
}) {
return (
<button
className={cx(
"pl-3 py-1 bg-gray-700 rounded-full flex justify-start items-center h-[34px]",
{ "pr-1": auth.viewerIdentity, "pr-3": !auth.viewerIdentity }
)}
onClick={() => setOpen(!open)}
>
<Eye />
<div className="text-white leading-snug ml-2 mr-1">Viewing</div>
<ChevronDown className="stroke-white w-[15px] h-[15px]" />
{auth.viewerIdentity && (
<ProfilePic
className="ml-2"
size="medium"
url={auth.viewerIdentity.profilePicUrl}
/>
)}
</button>
)
}
/**
* PopoverContentHeader is the header for the login popover.
*/
function PopoverContentHeader({ auth }: { auth: AuthResponse }) {
return (
<div className="text-black text-sm font-medium leading-tight mb-1">
{auth.authorized ? "Managing" : "Viewing"}
{auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
</div>
)
}
/**
* PopoverContentFooter is the footer for the login popover.
*/
function PopoverContentFooter({ auth }: { auth: AuthResponse }) {
return auth.viewerIdentity ? (
<>
<hr className="my-2" />
<div className="flex items-center">
<User className="flex-shrink-0" />
<p className="text-gray-500 text-xs ml-2">
We recognize you because you are accessing this page from{" "}
<span className="font-medium">
{auth.viewerIdentity.nodeName || auth.viewerIdentity.nodeIP}
</span>
</p>
</div>
</>
) : null
}
/**
* ReadonlyModeContent is the body of the login popover when the web
* client is being run in "readonly" server mode.
*/
function ReadonlyModeContent({ auth }: { auth: AuthResponse }) {
return (
<>
<PopoverContentHeader auth={auth} />
<p className="text-gray-500 text-xs">
This web interface is running in read-only mode.{" "}
<a
href="https://tailscale.com/s/web-client-read-only"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
<PopoverContentFooter auth={auth} />
</>
)
}
/**
* LoginModeContent is the body of the login popover when the web
* client is being run in "login" server mode.
*/
function LoginModeContent({
node,
auth,
tsWebConnected,
checkTSWebConnection,
}: {
node: NodeData
auth: AuthResponse
tsWebConnected: boolean
checkTSWebConnection: () => void
}) {
const https = isHTTPS()
// We can't run the ts web connection test when the webpage is loaded
// over HTTPS. So in this case, we default to presenting a login button
// with some helper text reminding the user to check their connection
// themselves.
const hasACLAccess = https || tsWebConnected
const hasEditCaps = useMemo(() => {
if (!auth.viewerIdentity) {
// If not connected to login client over tailscale, we won't know the viewer's
// identity. So we must assume they may be able to edit something and have the
// management client handle permissions once the user gets there.
return true
}
return hasAnyEditCapabilities(auth)
}, [auth])
const handleLogin = useCallback(() => {
// Must be connected over Tailscale to log in.
// Send user to Tailscale IP and start check mode
const manageURL = `http://${node.IPv4}:5252/?check=now`
if (window.self !== window.top) {
// If we're inside an iframe, open management client in new window.
window.open(manageURL, "_blank")
} else {
window.location.href = manageURL
}
}, [node.IPv4])
return (
<div
onMouseEnter={
hasEditCaps && !hasACLAccess ? checkTSWebConnection : undefined
}
>
<PopoverContentHeader auth={auth} />
{!hasACLAccess || !hasEditCaps ? (
<>
<p className="text-gray-500 text-xs">
{!hasEditCaps ? (
// ACLs allow access, but user isn't allowed to edit any features,
// restricted to readonly. No point in sending them over to the
// tailscaleIP:5252 address.
<>
You dont have permission to make changes to this device, but
you can view most of its details.
</>
) : !node.ACLAllowsAnyIncomingTraffic ? (
// Tailnet ACLs don't allow access to anyone.
<>
The current tailnet policy file does not allow connecting to
this device.
</>
) : (
// ACLs don't allow access to this user specifically.
<>
Cannot access this devices Tailscale IP. Make sure you are
connected to your tailnet, and that your policy file allows
access.
</>
)}{" "}
<a
href="https://tailscale.com/s/web-client-access"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
</>
) : (
// User can connect to Tailcale IP; sign in when ready.
<>
<p className="text-gray-500 text-xs">
You can see most of this devices details. To make changes, you need
to sign in.
</p>
{https && (
// we don't know if the user can connect over TS, so
// provide extra tips in case they have trouble.
<p className="text-gray-500 text-xs font-semibold pt-2">
Make sure you are connected to your tailnet, and that your policy
file allows access.
</p>
)}
<SignInButton auth={auth} onClick={handleLogin} />
</>
)}
<PopoverContentFooter auth={auth} />
</div>
)
}
/**
* ManageModeContent is the body of the login popover when the web
* client is being run in "manage" server mode.
*/
function ManageModeContent({
auth,
newSession,
}: {
node: NodeData
auth: AuthResponse
newSession: () => void
}) {
const handleLogin = useCallback(() => {
if (window.self !== window.top) {
// If we're inside an iframe, start session in new window.
let url = new URL(window.location.href)
url.searchParams.set("check", "now")
window.open(url, "_blank")
} else {
newSession()
}
}, [newSession])
const hasAnyPermissions = useMemo(() => hasAnyEditCapabilities(auth), [auth])
return (
<>
<PopoverContentHeader auth={auth} />
{!auth.authorized &&
(hasAnyPermissions ? (
// User is connected over Tailscale, but needs to complete check mode.
<>
<p className="text-gray-500 text-xs">
To make changes, sign in to confirm your identity. This extra step
helps us keep your device secure.
</p>
<SignInButton auth={auth} onClick={handleLogin} />
</>
) : (
// User is connected over tailscale, but doesn't have permission to manage.
<p className="text-gray-500 text-xs">
You dont have permission to make changes to this device, but you
can view most of its details.{" "}
<a
href="https://tailscale.com/s/web-client-access"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
))}
<PopoverContentFooter auth={auth} />
</>
)
}
function SignInButton({
auth,
onClick,
}: {
auth: AuthResponse
onClick: () => void
}) {
return (
<Button
className={cx("text-center w-full mt-2", {
"mb-2": auth.viewerIdentity,
})}
intent="primary"
sizeVariant="small"
onClick={onClick}
>
{auth.viewerIdentity ? "Sign in to confirm identity" : "Sign in"}
</Button>
)
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
import { isTailscaleIPv6 } from "src/utils/util"
type Props = {
ip: string
className?: string
}
/**
* NiceIP displays IP addresses with nice truncation.
*/
export default function NiceIP(props: Props) {
const { ip, className } = props
if (!isTailscaleIPv6(ip)) {
return <span className={className}>{ip}</span>
}
const [trimmable, untrimmable] = splitIPv6(ip)
return (
<span
className={cx("inline-flex justify-start min-w-0 max-w-full", className)}
>
{trimmable.length > 0 && (
<span className="truncate w-fit flex-shrink">{trimmable}</span>
)}
<span className="flex-grow-0 flex-shrink-0">{untrimmable}</span>
</span>
)
}
/**
* Split an IPv6 address into two pieces, to help with truncating the middle.
* Only exported for testing purposes. Do not use.
*/
export function splitIPv6(ip: string): [string, string] {
// We want to split the IPv6 address into segments, but not remove the delimiter.
// So we inject an invalid IPv6 character ("|") as a delimiter into the string,
// then split on that.
const parts = ip.replace(/(:{1,2})/g, "|$1").split("|")
// Then we find the number of end parts that fits within the character limit,
// and join them back together.
const characterLimit = 12
let characterCount = 0
let idxFromEnd = 1
for (let i = parts.length - 1; i >= 0; i--) {
const part = parts[i]
if (characterCount + part.length > characterLimit) {
break
}
characterCount += part.length
idxFromEnd++
}
const start = parts.slice(0, -idxFromEnd).join("")
const end = parts.slice(-idxFromEnd).join("")
return [start, end]
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import { VersionInfo } from "src/types"
import Button from "src/ui/button"
import Card from "src/ui/card"
import { useLocation } from "wouter"
export function UpdateAvailableNotification({
details,
}: {
details: VersionInfo
}) {
const [, setLocation] = useLocation()
return (
<Card>
<h2 className="mb-2">
Update available{" "}
{details.LatestVersion && `(v${details.LatestVersion})`}
</h2>
<p className="text-sm mb-1 mt-1">
{details.LatestVersion
? `Version ${details.LatestVersion}`
: "A new update"}{" "}
is now available. <ChangelogText version={details.LatestVersion} />
</p>
<Button
className="mt-3 inline-block"
sizeVariant="small"
onClick={() => setLocation("/update")}
>
Update now
</Button>
</Card>
)
}
// isStableTrack takes a Tailscale version string
// of form X.Y.Z (or vX.Y.Z) and returns whether
// it is a stable release (even value of Y)
// or unstable (odd value of Y).
// eg. isStableTrack("1.48.0") === true
// eg. isStableTrack("1.49.112") === false
function isStableTrack(ver: string): boolean {
const middle = ver.split(".")[1]
if (middle && Number(middle) % 2 === 0) {
return true
}
return false
}
export function ChangelogText({ version }: { version?: string }) {
if (!version || !isStableTrack(version)) {
return null
}
return (
<>
Check out the{" "}
<a href="https://tailscale.com/changelog/" className="link">
release notes
</a>{" "}
to find out whats new!
</>
)
}

View File

@@ -0,0 +1,249 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
import { useAPI } from "src/api"
import ACLTag from "src/components/acl-tag"
import * as Control from "src/components/control-components"
import NiceIP from "src/components/nice-ip"
import { UpdateAvailableNotification } from "src/components/update-available"
import { AuthResponse, canEdit } from "src/hooks/auth"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Card from "src/ui/card"
import Dialog from "src/ui/dialog"
import QuickCopy from "src/ui/quick-copy"
import { useLocation } from "wouter"
export default function DeviceDetailsView({
node,
auth,
}: {
node: NodeData
auth: AuthResponse
}) {
return (
<>
<h1 className="mb-10">Device details</h1>
<div className="flex flex-col gap-4">
<Card noPadding className="-mx-5 p-5 details-card">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h1>{node.DeviceName}</h1>
<div
className={cx("w-2.5 h-2.5 rounded-full", {
"bg-emerald-500": node.Status === "Running",
"bg-gray-300": node.Status !== "Running",
})}
/>
</div>
{canEdit("account", auth) && <DisconnectDialog />}
</div>
</Card>
{node.Features["auto-update"] &&
canEdit("account", auth) &&
node.ClientVersion &&
!node.ClientVersion.RunningLatest && (
<UpdateAvailableNotification details={node.ClientVersion} />
)}
<Card noPadding className="-mx-5 p-5 details-card">
<h2 className="mb-2">General</h2>
<table>
<tbody>
<tr className="flex">
<td>Managed by</td>
<td className="flex gap-1 flex-wrap">
{node.IsTagged
? node.Tags.map((t) => <ACLTag key={t} tag={t} />)
: node.Profile?.DisplayName}
</td>
</tr>
<tr>
<td>Machine name</td>
<td>
<QuickCopy
primaryActionValue={node.DeviceName}
primaryActionSubject="machine name"
>
{node.DeviceName}
</QuickCopy>
</td>
</tr>
<tr>
<td>OS</td>
<td>{node.OS}</td>
</tr>
<tr>
<td>ID</td>
<td>
<QuickCopy
primaryActionValue={node.ID}
primaryActionSubject="ID"
>
{node.ID}
</QuickCopy>
</td>
</tr>
<tr>
<td>Tailscale version</td>
<td>{node.IPNVersion}</td>
</tr>
<tr>
<td>Key expiry</td>
<td>
{node.KeyExpired
? "Expired"
: // TODO: present as relative expiry (e.g. "5 months from now")
node.KeyExpiry
? new Date(node.KeyExpiry).toLocaleString()
: "No expiry"}
</td>
</tr>
</tbody>
</table>
</Card>
<Card noPadding className="-mx-5 p-5 details-card">
<h2 className="mb-2">Addresses</h2>
<table>
<tbody>
<tr>
<td>Tailscale IPv4</td>
<td>
<QuickCopy
primaryActionValue={node.IPv4}
primaryActionSubject="IPv4 address"
>
{node.IPv4}
</QuickCopy>
</td>
</tr>
<tr>
<td>Tailscale IPv6</td>
<td>
<QuickCopy
primaryActionValue={node.IPv6}
primaryActionSubject="IPv6 address"
>
<NiceIP ip={node.IPv6} />
</QuickCopy>
</td>
</tr>
<tr>
<td>Short domain</td>
<td>
<QuickCopy
primaryActionValue={node.DeviceName}
primaryActionSubject="short domain"
>
{node.DeviceName}
</QuickCopy>
</td>
</tr>
<tr>
<td>Full domain</td>
<td>
<QuickCopy
primaryActionValue={`${node.DeviceName}.${node.TailnetName}`}
primaryActionSubject="full domain"
>
{node.DeviceName}.{node.TailnetName}
</QuickCopy>
</td>
</tr>
</tbody>
</table>
</Card>
<Card noPadding className="-mx-5 p-5 details-card">
<h2 className="mb-2">Debug</h2>
<table>
<tbody>
<tr>
<td>TUN Mode</td>
<td>{node.TUNMode ? "Yes" : "No"}</td>
</tr>
{node.IsSynology && (
<tr>
<td>Synology Version</td>
<td>{node.DSMVersion}</td>
</tr>
)}
</tbody>
</table>
</Card>
<footer className="text-gray-500 text-sm leading-tight text-center">
<Control.AdminContainer node={node}>
Want even more details? Visit{" "}
<Control.AdminLink node={node} path={`/machines/${node.IPv4}`}>
this devices page
</Control.AdminLink>{" "}
in the admin console.
</Control.AdminContainer>
<p className="mt-12">
<a
className="link"
href={node.LicensesURL}
target="_blank"
rel="noreferrer"
>
Acknowledgements
</a>{" "}
·{" "}
<a
className="link"
href="https://tailscale.com/privacy-policy/"
target="_blank"
rel="noreferrer"
>
Privacy Policy
</a>{" "}
·{" "}
<a
className="link"
href="https://tailscale.com/terms/"
target="_blank"
rel="noreferrer"
>
Terms of Service
</a>
</p>
<p className="my-2">
WireGuard is a registered trademark of Jason A. Donenfeld.
</p>
<p>
© {new Date().getFullYear()} Tailscale Inc. All rights reserved.
Tailscale is a registered trademark of Tailscale Inc.
</p>
</footer>
</div>
</>
)
}
function DisconnectDialog() {
const api = useAPI()
const [, setLocation] = useLocation()
return (
<Dialog
className="max-w-md"
title="Log out"
trigger={<Button sizeVariant="small">Log out</Button>}
>
<Dialog.Form
cancelButton
submitButton="Log out"
destructive
onSubmit={() => {
api({ action: "logout" })
setLocation("/disconnected")
}}
>
Logging out of this device will disconnect it from your tailnet and
expire its node key. You wont be able to use this web interface until
you re-authenticate the device from either the Tailscale app or the
Tailscale command line interface.
</Dialog.Form>
</Dialog>
)
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import TailscaleIcon from "src/assets/icons/tailscale-icon.svg?react"
/**
* DisconnectedView is rendered after node logout.
*/
export default function DisconnectedView() {
return (
<>
<TailscaleIcon className="mx-auto" />
<p className="mt-12 text-center text-text-muted">
You logged out of this device. To reconnect it you will have to
re-authenticate the device from either the Tailscale app or the
Tailscale command line interface.
</p>
</>
)
}

View File

@@ -0,0 +1,189 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useMemo } from "react"
import { apiFetch } from "src/api"
import ArrowRight from "src/assets/icons/arrow-right.svg?react"
import Machine from "src/assets/icons/machine.svg?react"
import AddressCard from "src/components/address-copy-card"
import ExitNodeSelector from "src/components/exit-node-selector"
import { AuthResponse, canEdit } from "src/hooks/auth"
import { NodeData } from "src/types"
import Card from "src/ui/card"
import { pluralize } from "src/utils/util"
import { Link, useLocation } from "wouter"
export default function HomeView({
node,
auth,
}: {
node: NodeData
auth: AuthResponse
}) {
const [allSubnetRoutes, pendingSubnetRoutes] = useMemo(
() => [
node.AdvertisedRoutes?.length,
node.AdvertisedRoutes?.filter((r) => !r.Approved).length,
],
[node.AdvertisedRoutes]
)
return (
<div className="mb-12 w-full">
<h2 className="mb-3">This device</h2>
<Card noPadding className="-mx-5 p-5 mb-9">
<div className="flex justify-between items-center text-lg mb-5">
<Link className="flex items-center" to="/details">
<div className="w-10 h-10 bg-gray-100 rounded-full justify-center items-center inline-flex">
<Machine />
</div>
<div className="ml-3">
<div className="text-gray-800 text-lg font-medium leading-snug">
{node.DeviceName}
</div>
<p className="text-gray-500 text-sm leading-[18.20px] flex items-center gap-2">
<span
className={cx("w-2 h-2 inline-block rounded-full", {
"bg-green-300": node.Status === "Running",
"bg-gray-300": node.Status !== "Running",
})}
/>
{node.Status === "Running" ? "Connected" : "Offline"}
</p>
</div>
</Link>
<AddressCard
className="-mr-2"
triggerClassName="relative text-gray-800 text-lg leading-[25.20px]"
v4Address={node.IPv4}
v6Address={node.IPv6}
shortDomain={node.DeviceName}
fullDomain={`${node.DeviceName}.${node.TailnetName}`}
/>
</div>
{(node.Features["advertise-exit-node"] ||
node.Features["use-exit-node"]) && (
<ExitNodeSelector
className="mb-5"
node={node}
disabled={!canEdit("exitnodes", auth)}
/>
)}
<Link
className="link font-medium"
to="/details"
onClick={() => apiFetch("/device-details-click", "POST")}
>
View device details &rarr;
</Link>
</Card>
<h2 className="mb-3">Settings</h2>
<div className="grid gap-3">
{node.Features["advertise-routes"] && (
<SettingsCard
link="/subnets"
title="Subnet router"
body="Add devices to your tailnet without installing Tailscale on them."
badge={
allSubnetRoutes
? {
text: `${allSubnetRoutes} ${pluralize(
"route",
"routes",
allSubnetRoutes
)}`,
}
: undefined
}
footer={
pendingSubnetRoutes
? `${pendingSubnetRoutes} ${pluralize(
"route",
"routes",
pendingSubnetRoutes
)} pending approval`
: undefined
}
/>
)}
{node.Features["ssh"] && (
<SettingsCard
link="/ssh"
title="Tailscale SSH server"
body="Run a Tailscale SSH server on this device and allow other devices in your tailnet to SSH into it."
badge={
node.RunningSSHServer
? {
text: "Running",
icon: <div className="w-2 h-2 bg-green-300 rounded-full" />,
}
: undefined
}
/>
)}
{/* TODO(sonia,will): hiding unimplemented settings pages until implemented */}
{/* <SettingsCard
link="/serve"
title="Share local content"
body="Share local ports, services, and content to your Tailscale network or to the broader internet."
/> */}
</div>
</div>
)
}
function SettingsCard({
title,
link,
body,
badge,
footer,
className,
}: {
title: string
link: string
body: string
badge?: {
text: string
icon?: JSX.Element
}
footer?: string
className?: string
}) {
const [, setLocation] = useLocation()
return (
<button onClick={() => setLocation(link)}>
<Card noPadding className={cx("-mx-5 p-5", className)}>
<div className="flex justify-between items-center">
<div>
<div className="flex gap-2">
<p className="text-gray-800 font-medium leading-tight mb-2">
{title}
</p>
{badge && (
<div className="h-5 px-2 bg-gray-100 rounded-full flex items-center gap-2">
{badge.icon}
<div className="text-gray-500 text-xs font-medium">
{badge.text}
</div>
</div>
)}
</div>
<p className="text-gray-500 text-sm leading-tight">{body}</p>
</div>
<div>
<ArrowRight className="ml-3" />
</div>
</div>
{footer && (
<>
<hr className="my-3" />
<div className="text-gray-500 text-sm leading-tight">{footer}</div>
</>
)}
</Card>
</button>
)
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React, { useState } from "react"
import { useAPI } from "src/api"
import TailscaleIcon from "src/assets/icons/tailscale-icon.svg?react"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Collapsible from "src/ui/collapsible"
import Input from "src/ui/input"
/**
* LoginView is rendered when the client is not authenticated
* to a tailnet.
*/
export default function LoginView({ data }: { data: NodeData }) {
const api = useAPI()
const [controlURL, setControlURL] = useState<string>("")
const [authKey, setAuthKey] = useState<string>("")
return (
<div className="mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<TailscaleIcon className="my-2 mb-8" />
{data.Status === "Stopped" ? (
<>
<div className="mb-6">
<h3 className="text-3xl font-semibold mb-3">Connect</h3>
<p className="text-gray-700">
Your device is disconnected from Tailscale.
</p>
</div>
<Button
onClick={() => api({ action: "up", data: {} })}
className="w-full mb-4"
intent="primary"
>
Connect to Tailscale
</Button>
</>
) : data.IPv4 ? (
<>
<div className="mb-6">
<p className="text-gray-700">
Your devices key has expired. Reauthenticate this device by
logging in again, or{" "}
<a
href="https://tailscale.com/kb/1028/key-expiry"
className="link"
target="_blank"
rel="noreferrer"
>
learn more
</a>
.
</p>
</div>
<Button
onClick={() =>
api({ action: "up", data: { Reauthenticate: true } })
}
className="w-full mb-4"
intent="primary"
>
Reauthenticate
</Button>
</>
) : (
<>
<div className="mb-6">
<h3 className="text-3xl font-semibold mb-3">Log in</h3>
<p className="text-gray-700">
Get started by logging in to your Tailscale network.
Or,&nbsp;learn&nbsp;more at{" "}
<a
href="https://tailscale.com/"
className="link"
target="_blank"
rel="noreferrer"
>
tailscale.com
</a>
.
</p>
</div>
<Button
onClick={() =>
api({
action: "up",
data: {
Reauthenticate: true,
ControlURL: controlURL,
AuthKey: authKey,
},
})
}
className="w-full mb-4"
intent="primary"
>
Log In
</Button>
<Collapsible trigger="Advanced options">
<h4 className="font-medium mb-1 mt-2">Auth Key</h4>
<p className="text-sm text-gray-500">
Connect with a pre-authenticated key.{" "}
<a
href="https://tailscale.com/kb/1085/auth-keys/"
className="link"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
<Input
className="mt-2"
value={authKey}
onChange={(e) => setAuthKey(e.target.value)}
placeholder="tskey-auth-XXX"
/>
<h4 className="font-medium mt-3 mb-1">Server URL</h4>
<p className="text-sm text-gray-500">Base URL of control server.</p>
<Input
className="mt-2"
value={controlURL}
onChange={(e) => setControlURL(e.target.value)}
placeholder="https://login.tailscale.com/"
/>
</Collapsible>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
import { useAPI } from "src/api"
import * as Control from "src/components/control-components"
import { NodeData } from "src/types"
import Card from "src/ui/card"
import Toggle from "src/ui/toggle"
export default function SSHView({
readonly,
node,
}: {
readonly: boolean
node: NodeData
}) {
const api = useAPI()
return (
<>
<h1 className="mb-1">Tailscale SSH server</h1>
<p className="description mb-10">
Run a Tailscale SSH server on this device and allow other devices in
your tailnet to SSH into it.{" "}
<a
href="https://tailscale.com/kb/1193/tailscale-ssh/"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
<Card noPadding className="-mx-5 p-5">
{!readonly ? (
<label className="flex gap-3 items-center">
<Toggle
checked={node.RunningSSHServer}
onChange={() =>
api({
action: "update-prefs",
data: {
RunSSHSet: true,
RunSSH: !node.RunningSSHServer,
},
})
}
/>
<div className="text-black text-sm font-medium leading-tight">
Run Tailscale SSH server
</div>
</label>
) : (
<div className="inline-flex items-center gap-3">
<span
className={cx("w-2 h-2 rounded-full", {
"bg-green-300": node.RunningSSHServer,
"bg-gray-300": !node.RunningSSHServer,
})}
/>
{node.RunningSSHServer ? "Running" : "Not running"}
</div>
)}
</Card>
{node.RunningSSHServer && (
<Control.AdminContainer
className="text-gray-500 text-sm leading-tight mt-3"
node={node}
>
Remember to make sure that the{" "}
<Control.AdminLink node={node} path="/acls">
tailnet policy file
</Control.AdminLink>{" "}
allows other devices to SSH into this device.
</Control.AdminContainer>
)}
</>
)
}

View File

@@ -0,0 +1,198 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useCallback, useMemo, useState } from "react"
import { useAPI } from "src/api"
import CheckCircle from "src/assets/icons/check-circle.svg?react"
import Clock from "src/assets/icons/clock.svg?react"
import Plus from "src/assets/icons/plus.svg?react"
import * as Control from "src/components/control-components"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Card from "src/ui/card"
import Dialog from "src/ui/dialog"
import EmptyState from "src/ui/empty-state"
import Input from "src/ui/input"
export default function SubnetRouterView({
readonly,
node,
}: {
readonly: boolean
node: NodeData
}) {
const api = useAPI()
const [advertisedRoutes, hasRoutes, hasUnapprovedRoutes] = useMemo(() => {
const routes = node.AdvertisedRoutes || []
return [routes, routes.length > 0, routes.find((r) => !r.Approved)]
}, [node.AdvertisedRoutes])
const [inputOpen, setInputOpen] = useState<boolean>(
advertisedRoutes.length === 0 && !readonly
)
const [inputText, setInputText] = useState<string>("")
const [postError, setPostError] = useState<string>()
const resetInput = useCallback(() => {
setInputText("")
setPostError("")
setInputOpen(false)
}, [])
return (
<>
<h1 className="mb-1">Subnet router</h1>
<p className="description mb-5">
Add devices to your tailnet without installing Tailscale.{" "}
<a
href="https://tailscale.com/kb/1019/subnets/"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
{!readonly &&
(inputOpen ? (
<Card noPadding className="-mx-5 p-5 !border-0 shadow-popover">
<p className="font-medium leading-snug mb-3">
Advertise new routes
</p>
<Input
type="text"
className="text-sm"
placeholder="192.168.0.0/24"
value={inputText}
onChange={(e) => {
setPostError("")
setInputText(e.target.value)
}}
/>
<p
className={cx("my-2 h-6 text-sm leading-tight", {
"text-gray-500": !postError,
"text-red-400": postError,
})}
>
{postError ||
"Add multiple routes by providing a comma-separated list."}
</p>
<div className="flex gap-3">
<Button
intent="primary"
onClick={() =>
api({
action: "update-routes",
data: [
...advertisedRoutes,
...inputText
.split(",")
.map((r) => ({ Route: r, Approved: false })),
],
})
.then(resetInput)
.catch((err: Error) => setPostError(err.message))
}
disabled={!inputText || postError !== ""}
>
Advertise {hasRoutes && "new "}routes
</Button>
{hasRoutes && <Button onClick={resetInput}>Cancel</Button>}
</div>
</Card>
) : (
<Button
intent="primary"
prefixIcon={<Plus />}
onClick={() => setInputOpen(true)}
>
Advertise new routes
</Button>
))}
<div className="-mx-5 mt-10">
{hasRoutes ? (
<>
<Card noPadding className="px-5 py-3">
{advertisedRoutes.map((r) => (
<div
className="flex justify-between items-center pb-2.5 mb-2.5 border-b border-b-gray-200 last:pb-0 last:mb-0 last:border-b-0"
key={r.Route}
>
<div className="text-gray-800 leading-snug">{r.Route}</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
{r.Approved ? (
<CheckCircle className="w-4 h-4" />
) : (
<Clock className="w-4 h-4" />
)}
{r.Approved ? (
<div className="text-green-500 text-sm leading-tight">
Approved
</div>
) : (
<div className="text-gray-500 text-sm leading-tight">
Pending approval
</div>
)}
</div>
{!readonly && (
<StopAdvertisingDialog
onSubmit={() =>
api({
action: "update-routes",
data: advertisedRoutes.filter(
(it) => it.Route !== r.Route
),
})
}
/>
)}
</div>
</div>
))}
</Card>
{hasUnapprovedRoutes && (
<Control.AdminContainer
className="mt-3 w-full text-center text-gray-500 text-sm leading-tight"
node={node}
>
To approve routes, in the admin console go to{" "}
<Control.AdminLink node={node} path={`/machines/${node.IPv4}`}>
the machines route settings
</Control.AdminLink>
.
</Control.AdminContainer>
)}
</>
) : (
<Card empty>
<EmptyState description="Not advertising any routes" />
</Card>
)}
</div>
</>
)
}
function StopAdvertisingDialog({ onSubmit }: { onSubmit: () => void }) {
return (
<Dialog
className="max-w-md"
title="Stop advertising route"
trigger={<Button sizeVariant="small">Stop advertising</Button>}
>
<Dialog.Form
cancelButton
submitButton="Stop advertising"
destructive
onSubmit={onSubmit}
>
Any active connections between devices over this route will be broken.
</Dialog.Form>
</Dialog>
)
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import CheckCircleIcon from "src/assets/icons/check-circle.svg?react"
import XCircleIcon from "src/assets/icons/x-circle.svg?react"
import { ChangelogText } from "src/components/update-available"
import { UpdateState, useInstallUpdate } from "src/hooks/self-update"
import { VersionInfo } from "src/types"
import Button from "src/ui/button"
import Spinner from "src/ui/spinner"
import { useLocation } from "wouter"
/**
* UpdatingView is rendered when the user initiates a Tailscale update, and
* the update is in-progress, failed, or completed.
*/
export function UpdatingView({
versionInfo,
currentVersion,
}: {
versionInfo?: VersionInfo
currentVersion: string
}) {
const [, setLocation] = useLocation()
const { updateState, updateLog } = useInstallUpdate(
currentVersion,
versionInfo
)
return (
<>
<div className="flex-1 flex flex-col justify-center items-center text-center mt-56">
{updateState === UpdateState.InProgress ? (
<>
<Spinner size="sm" className="text-gray-400" />
<h1 className="text-2xl m-3">Update in progress</h1>
<p className="text-gray-400">
The update shouldnt take more than a couple of minutes. Once its
completed, you will be asked to log in again.
</p>
</>
) : updateState === UpdateState.Complete ? (
<>
<CheckCircleIcon />
<h1 className="text-2xl m-3">Update complete!</h1>
<p className="text-gray-400">
You updated Tailscale
{versionInfo && versionInfo.LatestVersion
? ` to ${versionInfo.LatestVersion}`
: null}
. <ChangelogText version={versionInfo?.LatestVersion} />
</p>
<Button
className="m-3"
sizeVariant="small"
onClick={() => setLocation("/")}
>
Log in to access
</Button>
</>
) : updateState === UpdateState.UpToDate ? (
<>
<CheckCircleIcon />
<h1 className="text-2xl m-3">Up to date!</h1>
<p className="text-gray-400">
You are already running Tailscale {currentVersion}, which is the
newest version available.
</p>
<Button
className="m-3"
sizeVariant="small"
onClick={() => setLocation("/")}
>
Return
</Button>
</>
) : (
/* TODO(naman,sonia): Figure out the body copy and design for this view. */
<>
<XCircleIcon />
<h1 className="text-2xl m-3">Update failed</h1>
<p className="text-gray-400">
Update
{versionInfo && versionInfo.LatestVersion
? ` to ${versionInfo.LatestVersion}`
: null}{" "}
failed.
</p>
<Button
className="m-3"
sizeVariant="small"
onClick={() => setLocation("/")}
>
Return
</Button>
</>
)}
<pre className="h-64 overflow-scroll m-3">
<code>{updateLog}</code>
</pre>
</div>
</>
)
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useCallback, useEffect, useState } from "react"
import { apiFetch, setSynoToken } from "src/api"
export type AuthResponse = {
serverMode: AuthServerMode
authorized: boolean
viewerIdentity?: {
loginName: string
nodeName: string
nodeIP: string
profilePicUrl?: string
capabilities: { [key in PeerCapability]: boolean }
}
needsSynoAuth?: boolean
}
export type AuthServerMode = "login" | "readonly" | "manage"
export type PeerCapability = "*" | "ssh" | "subnets" | "exitnodes" | "account"
/**
* canEdit reports whether the given auth response specifies that the viewer
* has the ability to edit the given capability.
*/
export function canEdit(cap: PeerCapability, auth: AuthResponse): boolean {
if (!auth.authorized || !auth.viewerIdentity) {
return false
}
if (auth.viewerIdentity.capabilities["*"] === true) {
return true // can edit all features
}
return auth.viewerIdentity.capabilities[cap] === true
}
/**
* hasAnyEditCapabilities reports whether the given auth response specifies
* that the viewer has at least one edit capability. If this is true, the
* user is able to go through the auth flow to authenticate a management
* session.
*/
export function hasAnyEditCapabilities(auth: AuthResponse): boolean {
return Object.values(auth.viewerIdentity?.capabilities || {}).includes(true)
}
/**
* useAuth reports and refreshes Tailscale auth status for the web client.
*/
export default function useAuth() {
const [data, setData] = useState<AuthResponse>()
const [loading, setLoading] = useState<boolean>(true)
const [ranSynoAuth, setRanSynoAuth] = useState<boolean>(false)
const loadAuth = useCallback(() => {
setLoading(true)
return apiFetch<AuthResponse>("/auth", "GET")
.then((d) => {
setData(d)
if (d.needsSynoAuth) {
fetch("/webman/login.cgi")
.then((r) => r.json())
.then((a) => {
setSynoToken(a.SynoToken)
setRanSynoAuth(true)
setLoading(false)
})
} else {
setLoading(false)
}
return d
})
.catch((error) => {
setLoading(false)
console.error(error)
})
}, [])
const newSession = useCallback(() => {
return apiFetch<{ authUrl?: string }>("/auth/session/new", "GET")
.then((d) => {
if (d.authUrl) {
window.open(d.authUrl, "_blank")
return apiFetch("/auth/session/wait", "GET")
}
})
.then(() => {
loadAuth()
})
.catch((error) => {
console.error(error)
})
}, [loadAuth])
useEffect(() => {
loadAuth().then((d) => {
if (!d) {
return
}
if (
!d.authorized &&
hasAnyEditCapabilities(d) &&
// Start auth flow immediately if browser has requested it.
new URLSearchParams(window.location.search).get("check") === "now"
) {
newSession()
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
loadAuth() // Refresh auth state after syno auth runs
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ranSynoAuth])
return {
data,
loading,
newSession,
}
}

View File

@@ -0,0 +1,204 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useMemo } from "react"
import {
CityCode,
CountryCode,
ExitNode,
ExitNodeLocation,
NodeData,
} from "src/types"
import useSWR from "swr"
export default function useExitNodes(node: NodeData, filter?: string) {
const { data } = useSWR<ExitNode[]>("/exit-nodes")
const { tailnetNodesSorted, locationNodesMap } = useMemo(() => {
// First going through exit nodes and splitting them into two groups:
// 1. tailnetNodes: exit nodes advertised by tailnet's own nodes
// 2. locationNodes: exit nodes advertised by non-tailnet Mullvad nodes
let tailnetNodes: ExitNode[] = []
const locationNodes = new Map<CountryCode, Map<CityCode, ExitNode[]>>()
if (!node.Features["use-exit-node"]) {
// early-return
return {
tailnetNodesSorted: tailnetNodes,
locationNodesMap: locationNodes,
}
}
data?.forEach((n) => {
const loc = n.Location
if (!loc) {
// 2023-11-15: Currently, if the node doesn't have
// location information, it is owned by the tailnet.
// Only Mullvad exit nodes have locations filled.
tailnetNodes.push({
...n,
Name: trimDNSSuffix(n.Name, node.TailnetName),
})
return
}
const countryNodes =
locationNodes.get(loc.CountryCode) || new Map<CityCode, ExitNode[]>()
const cityNodes = countryNodes.get(loc.CityCode) || []
countryNodes.set(loc.CityCode, [...cityNodes, n])
locationNodes.set(loc.CountryCode, countryNodes)
})
return {
tailnetNodesSorted: tailnetNodes.sort(compareByName),
locationNodesMap: locationNodes,
}
}, [data, node.Features, node.TailnetName])
const hasFilter = Boolean(filter)
const mullvadNodesSorted = useMemo(() => {
const nodes: ExitNode[] = []
if (!node.Features["use-exit-node"]) {
return nodes // early-return
}
// addBestMatchNode adds the node with the "higest priority"
// match from a list of exit node `options` to `nodes`.
const addBestMatchNode = (
options: ExitNode[],
name: (l: ExitNodeLocation) => string
) => {
const bestNode = highestPriorityNode(options)
if (!bestNode || !bestNode.Location) {
return // not possible, doing this for type safety
}
nodes.push({
...bestNode,
Name: name(bestNode.Location),
})
}
if (!hasFilter) {
// When nothing is searched, only show a single best-matching
// exit node per-country.
//
// There's too many location-based nodes to display all of them.
locationNodesMap.forEach(
// add one node per country
(countryNodes) =>
addBestMatchNode(flattenMap(countryNodes), (l) => l.Country)
)
} else {
// Otherwise, show the best match on a city-level,
// with a "Country: Best Match" node at top.
//
// i.e. We allow for discovering cities through searching.
locationNodesMap.forEach((countryNodes) => {
countryNodes.forEach(
// add one node per city
(cityNodes) =>
addBestMatchNode(cityNodes, (l) => `${l.Country}: ${l.City}`)
)
// add the "Country: Best Match" node
addBestMatchNode(
flattenMap(countryNodes),
(l) => `${l.Country}: Best Match`
)
})
}
return nodes.sort(compareByName)
}, [hasFilter, locationNodesMap, node.Features])
// Ordered and filtered grouping of exit nodes.
const exitNodeGroups = useMemo(() => {
const filterLower = !filter ? undefined : filter.toLowerCase()
const selfGroup = {
id: "self",
name: undefined,
nodes: filter
? []
: !node.Features["advertise-exit-node"]
? [noExitNode] // don't show "runAsExitNode" option
: [noExitNode, runAsExitNode],
}
if (!node.Features["use-exit-node"]) {
return [selfGroup]
}
return [
selfGroup,
{
id: "tailnet",
nodes: filterLower
? tailnetNodesSorted.filter((n) =>
n.Name.toLowerCase().includes(filterLower)
)
: tailnetNodesSorted,
},
{
id: "mullvad",
name: "Mullvad VPN",
nodes: filterLower
? mullvadNodesSorted.filter((n) =>
n.Name.toLowerCase().includes(filterLower)
)
: mullvadNodesSorted,
},
]
}, [filter, node.Features, tailnetNodesSorted, mullvadNodesSorted])
return { data: exitNodeGroups }
}
// highestPriorityNode finds the highest priority node for use
// (the "best match" node) from a list of exit nodes.
// Nodes with equal priorities are picked between arbitrarily.
function highestPriorityNode(nodes: ExitNode[]): ExitNode | undefined {
return nodes.length === 0
? undefined
: nodes.sort(
(a, b) => (b.Location?.Priority || 0) - (a.Location?.Priority || 0)
)[0]
}
// compareName compares two exit nodes alphabetically by name.
function compareByName(a: ExitNode, b: ExitNode): number {
if (a.Location && b.Location && a.Location.Country === b.Location.Country) {
// Always put "<Country>: Best Match" node at top of country list.
if (a.Name.includes(": Best Match")) {
return -1
} else if (b.Name.includes(": Best Match")) {
return 1
}
}
return a.Name.localeCompare(b.Name)
}
function flattenMap<T, V>(m: Map<T, V[]>): V[] {
return Array.from(m.values()).reduce((prev, curr) => [...prev, ...curr])
}
// trimDNSSuffix trims the tailnet dns name from s, leaving no
// trailing dots.
//
// trimDNSSuffix("hello.ts.net", "ts.net") = "hello"
// trimDNSSuffix("hello", "ts.net") = "hello"
export function trimDNSSuffix(s: string, tailnetDNSName: string): string {
if (s.endsWith(".")) {
s = s.slice(0, -1)
}
if (s.endsWith("." + tailnetDNSName)) {
s = s.replace("." + tailnetDNSName, "")
}
return s
}
// Neither of these are really "online", but setting this makes them selectable.
export const noExitNode: ExitNode = { ID: "NONE", Name: "None", Online: true }
export const runAsExitNode: ExitNode = {
ID: "RUNNING",
Name: "Run as exit node",
Online: true,
}

View File

@@ -1,117 +0,0 @@
import { useCallback, useEffect, useState } from "react"
import { apiFetch, setUnraidCsrfToken } from "src/api"
export type NodeData = {
Profile: UserProfile
Status: string
DeviceName: string
IP: string
AdvertiseExitNode: boolean
AdvertiseRoutes: string
LicensesURL: string
TUNMode: boolean
IsSynology: boolean
DSMVersion: number
IsUnraid: boolean
UnraidToken: string
IPNVersion: string
DebugMode: "" | "login" | "full" // empty when not running in any debug mode
}
export type UserProfile = {
LoginName: string
DisplayName: string
ProfilePicURL: string
}
export type NodeUpdate = {
AdvertiseRoutes?: string
AdvertiseExitNode?: boolean
Reauthenticate?: boolean
ForceLogout?: boolean
}
// useNodeData returns basic data about the current node.
export default function useNodeData() {
const [data, setData] = useState<NodeData>()
const [isPosting, setIsPosting] = useState<boolean>(false)
const refreshData = useCallback(
() =>
apiFetch("/data", "GET")
.then((r) => r.json())
.then((d: NodeData) => {
setData(d)
setUnraidCsrfToken(d.IsUnraid ? d.UnraidToken : undefined)
})
.catch((error) => console.error(error)),
[setData]
)
const updateNode = useCallback(
(update: NodeUpdate) => {
// The contents of this function are mostly copied over
// from the legacy client's web.html file.
// It makes all data updates through one API endpoint.
// As we build out the web client in React,
// this endpoint will eventually be deprecated.
if (isPosting || !data) {
return
}
setIsPosting(true)
update = {
...update,
// Default to current data value for any unset fields.
AdvertiseRoutes:
update.AdvertiseRoutes !== undefined
? update.AdvertiseRoutes
: data.AdvertiseRoutes,
AdvertiseExitNode:
update.AdvertiseExitNode !== undefined
? update.AdvertiseExitNode
: data.AdvertiseExitNode,
}
apiFetch("/data", "POST", update, { up: "true" })
.then((r) => r.json())
.then((r) => {
setIsPosting(false)
const err = r["error"]
if (err) {
throw new Error(err)
}
const url = r["url"]
if (url) {
window.open(url, "_blank")
}
refreshData()
})
.catch((err) => alert("Failed operation: " + err.message))
},
[data]
)
useEffect(
() => {
// Initial data load.
refreshData()
// Refresh on browser tab focus.
const onVisibilityChange = () => {
document.visibilityState === "visible" && refreshData()
}
window.addEventListener("visibilitychange", onVisibilityChange)
return () => {
// Cleanup browser tab listener.
window.removeEventListener("visibilitychange", onVisibilityChange)
}
},
// Run once.
[]
)
return { data, refreshData, updateNode, isPosting }
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useCallback, useEffect, useState } from "react"
import { apiFetch } from "src/api"
import { VersionInfo } from "src/types"
// see ipnstate.UpdateProgress
export type UpdateProgress = {
status: "UpdateFinished" | "UpdateInProgress" | "UpdateFailed"
message: string
version: string
}
export enum UpdateState {
UpToDate,
Available,
InProgress,
Complete,
Failed,
}
// useInstallUpdate initiates and tracks a Tailscale self-update via the LocalAPI,
// and returns state messages showing the progress of the update.
export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
const [updateState, setUpdateState] = useState<UpdateState>(
cv?.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
)
const [updateLog, setUpdateLog] = useState<string>("")
const appendUpdateLog = useCallback(
(msg: string) => {
setUpdateLog(updateLog + msg + "\n")
},
[updateLog, setUpdateLog]
)
useEffect(() => {
if (updateState !== UpdateState.Available) {
// useEffect cleanup function
return () => {}
}
setUpdateState(UpdateState.InProgress)
apiFetch("/local/v0/update/install", "POST").catch((err) => {
console.error(err)
setUpdateState(UpdateState.Failed)
})
let tsAwayForPolls = 0
let updateMessagesRead = 0
let timer: NodeJS.Timeout | undefined
function poll() {
apiFetch<UpdateProgress[]>("/local/v0/update/progress", "GET")
.then((res) => {
// res contains a list of UpdateProgresses that is strictly increasing
// in size, so updateMessagesRead keeps track (across calls of poll())
// of how many of those we have already read. This is why it is not
// initialized to zero here and we don't just use res.forEach()
for (; updateMessagesRead < res.length; ++updateMessagesRead) {
const up = res[updateMessagesRead]
if (up.status === "UpdateFailed") {
setUpdateState(UpdateState.Failed)
if (up.message) appendUpdateLog("ERROR: " + up.message)
return
}
if (up.status === "UpdateFinished") {
// if update finished and tailscaled did not go away (ie. did not restart),
// then the version being the same might not be an error, it might just require
// the user to restart Tailscale manually (this is required in some cases in the
// clientupdate package).
if (up.version === currentVersion && tsAwayForPolls > 0) {
setUpdateState(UpdateState.Failed)
appendUpdateLog(
"ERROR: Update failed, still running Tailscale " + up.version
)
if (up.message) appendUpdateLog("ERROR: " + up.message)
} else {
setUpdateState(UpdateState.Complete)
if (up.message) appendUpdateLog("INFO: " + up.message)
}
return
}
setUpdateState(UpdateState.InProgress)
if (up.message) appendUpdateLog("INFO: " + up.message)
}
// If we have gone through the entire loop without returning out of the function,
// the update is still in progress. So we want to poll again for further status
// updates.
timer = setTimeout(poll, 1000)
})
.catch((err) => {
++tsAwayForPolls
if (tsAwayForPolls >= 5 * 60) {
setUpdateState(UpdateState.Failed)
appendUpdateLog(
"ERROR: tailscaled went away but did not come back!"
)
appendUpdateLog("ERROR: last error received:")
appendUpdateLog(err.toString())
} else {
timer = setTimeout(poll, 1000)
}
})
}
poll()
// useEffect cleanup function
return () => {
if (timer) clearTimeout(timer)
timer = undefined
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return !cv
? { updateState: UpdateState.UpToDate, updateLog: "" }
: { updateState, updateLog }
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useRawToasterForHook } from "src/ui/toaster"
/**
* useToaster provides a mechanism to display toasts. It returns an object with
* methods to show, dismiss, or clear all toasts:
*
* const toastKey = toaster.show({ message: "Hello world" })
* toaster.dismiss(toastKey)
* toaster.clear()
*
*/
const useToaster = useRawToasterForHook
export default useToaster

View File

@@ -0,0 +1,46 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useCallback, useEffect, useState } from "react"
import { isHTTPS } from "src/utils/util"
import { AuthServerMode } from "./auth"
/**
* useTSWebConnected hook is used to check whether the browser is able to
* connect to the web client served at http://${nodeIPv4}:5252
*/
export function useTSWebConnected(mode: AuthServerMode, nodeIPv4: string) {
const [tsWebConnected, setTSWebConnected] = useState<boolean>(
mode === "manage" // browser already on the web client
)
const [isLoading, setIsLoading] = useState<boolean>(false)
const checkTSWebConnection = useCallback(() => {
if (mode === "manage") {
// Already connected to the web client.
setTSWebConnected(true)
return
}
if (isHTTPS()) {
// When page is loaded over HTTPS, the connectivity check will always
// fail with a mixed-content error. In this case don't bother doing
// the check.
return
}
if (isLoading) {
return // already checking
}
setIsLoading(true)
fetch(`http://${nodeIPv4}:5252/ok`, { mode: "no-cors" })
.then(() => {
setTSWebConnected(true)
setIsLoading(false)
})
.catch(() => setIsLoading(false))
}, [isLoading, mode, nodeIPv4])
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => checkTSWebConnection(), []) // checking connection for first time on page load
return { tsWebConnected, checkTSWebConnection, isLoading }
}

View File

@@ -1,15 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="20" fill="#F7F5F4"/>
<g clip-path="url(#clip0_13627_11903)">
<path d="M26.6666 11.6667H13.3333C12.4128 11.6667 11.6666 12.4129 11.6666 13.3333V16.6667C11.6666 17.5871 12.4128 18.3333 13.3333 18.3333H26.6666C27.5871 18.3333 28.3333 17.5871 28.3333 16.6667V13.3333C28.3333 12.4129 27.5871 11.6667 26.6666 11.6667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26.6666 21.6667H13.3333C12.4128 21.6667 11.6666 22.4129 11.6666 23.3333V26.6667C11.6666 27.5871 12.4128 28.3333 13.3333 28.3333H26.6666C27.5871 28.3333 28.3333 27.5871 28.3333 26.6667V23.3333C28.3333 22.4129 27.5871 21.6667 26.6666 21.6667Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 15H15.01" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 25H15.01" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<circle cx="34" cy="34" r="4.5" fill="#1EA672" stroke="white"/>
<defs>
<clipPath id="clip0_13627_11903">
<rect width="20" height="20" fill="white" transform="translate(10 10)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,129 +2,463 @@
@tailwind components;
@tailwind utilities;
/**
* Non-Tailwind styles begin here.
*/
@layer base {
@font-face {
font-family: "Inter";
font-weight: 100 900;
font-style: normal;
font-display: swap;
src: url("./assets/fonts/Inter.var.latin.woff2") format("woff2-variations");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+02BB-02BC, U+2000-206F, U+2122, U+2190-2199, U+2212, U+2215, U+FEFF,
U+FFFD, U+E06B-E080, U+02E2, U+02E2, U+02B0, U+1D34, U+1D57, U+1D40,
U+207F, U+1D3A, U+1D48, U+1D30, U+02B3, U+1D3F;
}
html {
/**
* These lines force the page to occupy the full width of the browser,
* ignoring the scrollbar, and prevent horizontal scrolling. This eliminates
* shifting when moving between pages with a scrollbar and those without, by
* ignoring the width of the scrollbar.
*
* It also disables horizontal scrolling of the body wholesale, so, as always
* avoid content flowing off the page.
*/
width: 100vw;
overflow-x: hidden;
}
.bg-gray-0 {
--tw-bg-opacity: 1;
background-color: rgba(250, 249, 248, var(--tw-bg-opacity));
:root {
--color-white: 255 255 255;
--color-gray-0: 250 249 248;
--color-gray-50: 249 247 246;
--color-gray-100: 247 245 244;
--color-gray-200: 238 235 234;
--color-gray-300: 218 214 213;
--color-gray-400: 175 172 171;
--color-gray-500: 112 110 109;
--color-gray-600: 68 67 66;
--color-gray-700: 46 45 45;
--color-gray-800: 35 34 34;
--color-gray-900: 31 30 30;
--color-red-0: 255 246 244;
--color-red-50: 255 211 207;
--color-red-100: 255 177 171;
--color-red-200: 246 143 135;
--color-red-300: 228 108 99;
--color-red-400: 208 72 65;
--color-red-500: 178 45 48;
--color-red-600: 148 8 33;
--color-red-700: 118 0 18;
--color-red-800: 90 0 0;
--color-red-900: 66 0 0;
--color-yellow-0: 252 249 233;
--color-yellow-50: 248 229 185;
--color-yellow-100: 239 192 120;
--color-yellow-200: 229 153 62;
--color-yellow-300: 217 121 23;
--color-yellow-400: 187 85 4;
--color-yellow-500: 152 55 5;
--color-yellow-600: 118 43 11;
--color-yellow-700: 87 31 13;
--color-yellow-800: 58 22 7;
--color-yellow-900: 58 22 7;
--color-orange-0: 255 250 238;
--color-orange-50: 254 227 192;
--color-orange-100: 248 184 134;
--color-orange-200: 245 146 94;
--color-orange-300: 229 111 74;
--color-orange-400: 196 76 52;
--color-orange-500: 158 47 40;
--color-orange-600: 126 30 35;
--color-orange-700: 93 22 27;
--color-orange-800: 66 14 17;
--color-orange-900: 66 14 17;
--color-green-0: 239 255 237;
--color-green-50: 203 244 201;
--color-green-100: 133 217 150;
--color-green-200: 51 194 127;
--color-green-300: 30 166 114;
--color-green-400: 9 130 93;
--color-green-500: 14 98 69;
--color-green-600: 13 75 59;
--color-green-700: 11 55 51;
--color-green-800: 8 36 41;
--color-green-900: 8 36 41;
--color-blue-0: 240 245 255;
--color-blue-50: 206 222 253;
--color-blue-100: 173 199 252;
--color-blue-200: 133 170 245;
--color-blue-300: 108 148 236;
--color-blue-400: 90 130 222;
--color-blue-500: 75 112 204;
--color-blue-600: 63 93 179;
--color-blue-700: 50 73 148;
--color-blue-800: 37 53 112;
--color-blue-900: 25 34 74;
--color-text-base: rgb(var(--color-gray-800) / 1);
--color-text-muted: rgb(var(--color-gray-500) / 1);
--color-text-disabled: rgb(var(--color-gray-400) / 1);
--color-text-primary: rgb(var(--color-blue-600) / 1);
--color-text-warning: rgb(var(--color-orange-600) / 1);
--color-text-danger: rgb(var(--color-red-600) / 1);
--color-bg-app: rgb(var(--color-gray-100) / 1);
--color-bg-menu-item-hover: rgb(var(--color-gray-100) / 1);
--color-border-base: rgb(var(--color-gray-200) / 1);
}
html,
body,
#app-root {
min-height: 100vh;
}
body {
@apply text-text-base font-sans w-full antialiased;
font-size: 16px;
line-height: 1.4;
letter-spacing: -0.015em; /* Inter is a little loose by default */
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
::selection {
background-color: rgba(97, 122, 255, 0.2);
}
strong {
@apply font-semibold;
}
button {
text-align: inherit; /* don't center buttons by default */
letter-spacing: inherit; /* inherit existing letter spacing, rather than using browser defaults */
vertical-align: top; /* fix alignment of display: inline-block buttons */
}
a:focus,
button:focus {
outline: none;
}
a:focus-visible,
button:focus-visible {
outline: auto;
}
h1 {
@apply text-gray-800 text-[22px] font-medium leading-[30.80px];
}
h2 {
@apply text-gray-500 text-sm font-medium uppercase leading-tight tracking-wide;
}
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgba(249, 247, 246, var(--tw-bg-opacity));
@layer components {
.details-card h1 {
@apply text-gray-800 text-lg font-medium leading-snug;
}
.details-card h2 {
@apply text-gray-500 text-xs font-semibold uppercase tracking-wide;
}
.details-card table {
@apply w-full;
}
.details-card tbody {
@apply flex flex-col gap-2;
}
.details-card tr {
@apply grid grid-flow-col grid-cols-3 gap-2;
}
.details-card td:first-child {
@apply text-gray-500 text-sm leading-tight truncate;
}
.details-card td:last-child {
@apply col-span-2 text-gray-800 text-sm leading-tight;
}
.description {
@apply text-gray-500 leading-snug;
}
/**
* .toggle applies "Toggle" UI styles to input[type="checkbox"] form elements.
* You can use the -large and -small modifiers for size variants.
*/
.toggle {
@apply appearance-none relative w-10 h-5 rounded-full bg-gray-300 cursor-pointer;
transition: background-color 200ms ease-in-out;
}
.toggle:disabled {
@apply bg-gray-200;
@apply cursor-not-allowed;
}
.toggle:checked {
@apply bg-blue-500;
}
.toggle:checked:disabled {
@apply bg-blue-300;
}
.toggle:focus {
@apply outline-none ring;
}
.toggle::after {
@apply absolute bg-white rounded-full will-change-[width];
@apply w-3.5 h-3.5 m-[0.1875rem] translate-x-0;
content: " ";
transition: width 200ms ease, transform 200ms ease;
}
.toggle:checked::after {
@apply translate-x-5;
}
.toggle:checked:disabled::after {
@apply bg-blue-50;
}
.toggle:enabled:active::after {
@apply w-[1.125rem];
}
.toggle:checked:enabled:active::after {
@apply w-[1.125rem] translate-x-3.5;
}
.toggle-large {
@apply w-12 h-6;
}
.toggle-large::after {
@apply m-1 w-4 h-4;
}
.toggle-large:checked::after {
@apply translate-x-6;
}
.toggle-large:enabled:active::after {
@apply w-6;
}
.toggle-large:checked:enabled:active::after {
@apply w-6 translate-x-4;
}
.toggle-small {
@apply w-6 h-3;
}
.toggle-small:focus {
/**
* We disable ring for .toggle-small because it is a
* small, inline element.
*/
@apply outline-none shadow-none;
}
.toggle-small::after {
@apply w-2 h-2 m-0.5;
}
.toggle-small:checked::after {
@apply translate-x-3;
}
.toggle-small:enabled:active::after {
@apply w-[0.675rem];
}
.toggle-small:checked:enabled:active::after {
@apply w-[0.675rem] translate-x-[0.55rem];
}
/**
* .button encapsulates all the base button styles we use across the app.
*/
.button {
@apply relative inline-flex flex-nowrap items-center justify-center font-medium py-2 px-4 rounded-md border border-transparent text-center whitespace-nowrap;
transition-property: background-color, border-color, color, box-shadow;
transition-duration: 120ms;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
}
.button:focus-visible {
@apply outline-none ring;
}
.button:disabled {
@apply pointer-events-none select-none;
}
.button-group {
@apply whitespace-nowrap;
}
.button-group .button {
@apply min-w-[60px];
}
.button-group .button:not(:first-child) {
@apply rounded-l-none;
}
.button-group .button:not(:last-child) {
@apply rounded-r-none border-r-0;
}
/**
* .input defines default text input field styling. These styles should
* correspond to .button, sharing a similar height and rounding, since .input
* and .button are commonly used together.
*/
.input,
.input-wrapper {
@apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors w-full h-input;
}
.input {
@apply px-3;
}
.input::placeholder,
.input-wrapper::placeholder {
@apply text-gray-400;
}
.input:disabled,
.input-wrapper:disabled {
@apply border-gray-300;
@apply bg-gray-0;
@apply cursor-not-allowed;
}
.input:focus,
.input-wrapper:focus-within {
@apply outline-none ring border-gray-400;
}
.input-error {
@apply border-red-200;
}
/**
* .loading-dots creates a set of three dots that pulse for indicating loading
* states where a more horizontal appearance is helpful.
*/
.loading-dots {
@apply inline-flex items-center;
}
.loading-dots span {
@apply inline-block w-[0.35rem] h-[0.35rem] rounded-full bg-current mx-[0.15em];
animation-name: loading-dots-blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
}
.loading-dots span:nth-child(2) {
animation-delay: 200ms;
}
.loading-dots span:nth-child(3) {
animation-delay: 400ms;
}
@keyframes loading-dots-blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
/**
* .spinner creates a circular animated spinner, most often used to indicate a
* loading state. The .spinner element must define a width, height, and
* border-width for the spinner to apply.
*/
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.spinner {
@apply border-transparent border-t-current border-l-current rounded-full;
animation: spin 700ms linear infinite;
}
/**
* .link applies standard styling to links across the app. By default we unstyle
* all anchor tags. While this might sound crazy for a website, it's _very_
* helpful in an app, since anchor tags can be used to wrap buttons, icons,
* and all manner of UI component. As a result, all anchor tags intended to look
* like links should have a .link class.
*/
.link {
@apply text-text-primary;
}
.link:hover,
.link:active {
@apply text-blue-700;
}
.link-destructive {
@apply text-text-danger;
}
.link-destructive:hover,
.link-destructive:active {
@apply text-red-700;
}
.link-fade {
}
.link-fade:hover {
@apply opacity-75;
}
.link-underline {
@apply underline;
}
.link-underline:hover {
@apply opacity-75;
}
}
html {
letter-spacing: -0.015em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.link {
--text-opacity: 1;
color: #4b70cc;
color: rgba(75, 112, 204, var(--text-opacity));
}
.link:hover,
.link:active {
--text-opacity: 1;
color: #19224a;
color: rgba(25, 34, 74, var(--text-opacity));
}
.link-underline {
text-decoration: underline;
}
.link-underline:hover,
.link-underline:active {
text-decoration: none;
}
.link-muted {
/* same as text-gray-500 */
--tw-text-opacity: 1;
color: rgba(112, 110, 109, var(--tw-text-opacity));
}
.link-muted:hover,
.link-muted:active {
/* same as text-gray-500 */
--tw-text-opacity: 1;
color: rgba(68, 67, 66, var(--tw-text-opacity));
}
.button {
font-weight: 500;
padding-top: 0.45rem;
padding-bottom: 0.45rem;
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0.375rem;
border-width: 1px;
border-color: transparent;
transition-property: background-color, border-color, color, box-shadow;
transition-duration: 120ms;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
min-width: 80px;
}
.button:focus {
outline: 0;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
.button:disabled {
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.button-blue {
--bg-opacity: 1;
background-color: #4b70cc;
background-color: rgba(75, 112, 204, var(--bg-opacity));
--border-opacity: 1;
border-color: #4b70cc;
border-color: rgba(75, 112, 204, var(--border-opacity));
--text-opacity: 1;
color: #fff;
color: rgba(255, 255, 255, var(--text-opacity));
}
.button-blue:enabled:hover {
--bg-opacity: 1;
background-color: #3f5db3;
background-color: rgba(63, 93, 179, var(--bg-opacity));
--border-opacity: 1;
border-color: #3f5db3;
border-color: rgba(63, 93, 179, var(--border-opacity));
}
.button-blue:disabled {
--text-opacity: 1;
color: #cedefd;
color: rgba(206, 222, 253, var(--text-opacity));
--bg-opacity: 1;
background-color: #6c94ec;
background-color: rgba(108, 148, 236, var(--bg-opacity));
--border-opacity: 1;
border-color: #6c94ec;
border-color: rgba(108, 148, 236, var(--border-opacity));
}
.button-red {
background-color: #d04841;
border-color: #d04841;
color: #fff;
}
.button-red:enabled:hover {
background-color: #b22d30;
border-color: #b22d30;
@layer utilities {
.h-input {
@apply h-[2.375rem];
}
}

View File

@@ -1,6 +1,19 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Preserved js license comment for web client app.
/**
* @license
* Copyright (c) Tailscale Inc & AUTHORS
* SPDX-License-Identifier: BSD-3-Clause
*/
import React from "react"
import { createRoot } from "react-dom/client"
import { swrConfig } from "src/api"
import App from "src/components/app"
import ToastProvider from "src/ui/toaster"
import { SWRConfig } from "swr"
declare var window: any
// This is used to determine if the react client is built.
@@ -15,6 +28,10 @@ const root = createRoot(rootEl)
root.render(
<React.StrictMode>
<App />
<SWRConfig value={swrConfig}>
<ToastProvider>
<App />
</ToastProvider>
</SWRConfig>
</React.StrictMode>
)

113
client/web/src/types.ts Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { assertNever } from "src/utils/util"
export type NodeData = {
Profile: UserProfile
Status: NodeState
DeviceName: string
OS: string
IPv4: string
IPv6: string
ID: string
KeyExpiry: string
KeyExpired: boolean
UsingExitNode?: ExitNode
AdvertisingExitNode: boolean
AdvertisingExitNodeApproved: boolean
AdvertisedRoutes?: SubnetRoute[]
TUNMode: boolean
IsSynology: boolean
DSMVersion: number
IsUnraid: boolean
UnraidToken: string
IPNVersion: string
ClientVersion?: VersionInfo
URLPrefix: string
DomainName: string
TailnetName: string
IsTagged: boolean
Tags: string[]
RunningSSHServer: boolean
ControlAdminURL: string
LicensesURL: string
Features: { [key in Feature]: boolean } // value is true if given feature is available on this client
ACLAllowsAnyIncomingTraffic: boolean
}
export type NodeState =
| "NoState"
| "NeedsLogin"
| "NeedsMachineAuth"
| "Stopped"
| "Starting"
| "Running"
export type UserProfile = {
LoginName: string
DisplayName: string
ProfilePicURL: string
}
export type SubnetRoute = {
Route: string
Approved: boolean
}
export type ExitNode = {
ID: string
Name: string
Location?: ExitNodeLocation
Online?: boolean
}
export type ExitNodeLocation = {
Country: string
CountryCode: CountryCode
City: string
CityCode: CityCode
Priority: number
}
export type CountryCode = string
export type CityCode = string
export type ExitNodeGroup = {
id: string
name?: string
nodes: ExitNode[]
}
export type Feature =
| "advertise-exit-node"
| "advertise-routes"
| "use-exit-node"
| "ssh"
| "auto-update"
export const featureDescription = (f: Feature) => {
switch (f) {
case "advertise-exit-node":
return "Advertising as an exit node"
case "advertise-routes":
return "Advertising subnet routes"
case "use-exit-node":
return "Using an exit node"
case "ssh":
return "Running a Tailscale SSH server"
case "auto-update":
return "Auto updating client versions"
default:
assertNever(f)
}
}
/**
* VersionInfo type is deserialized from tailcfg.ClientVersion,
* so it should not include fields not included in that type.
*/
export type VersionInfo = {
RunningLatest: boolean
LatestVersion?: string
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { HTMLAttributes } from "react"
export type BadgeColor =
| "blue"
| "green"
| "red"
| "orange"
| "yellow"
| "gray"
| "outline"
type Props = {
variant: "tag" | "status"
color: BadgeColor
} & HTMLAttributes<HTMLDivElement>
export default function Badge(props: Props) {
const { className, color, variant, ...rest } = props
return (
<div
className={cx(
"inline-flex items-center align-middle justify-center font-medium",
{
"border border-gray-200 bg-gray-200 text-gray-600": color === "gray",
"border border-green-50 bg-green-50 text-green-600":
color === "green",
"border border-blue-50 bg-blue-50 text-blue-600": color === "blue",
"border border-orange-50 bg-orange-50 text-orange-600":
color === "orange",
"border border-yellow-50 bg-yellow-50 text-yellow-600":
color === "yellow",
"border border-red-50 bg-red-50 text-red-600": color === "red",
"border border-gray-300 bg-white": color === "outline",
"rounded-full px-2 py-1 leading-none": variant === "status",
"rounded-sm px-1": variant === "tag",
},
className
)}
{...rest}
/>
)
}
Badge.defaultProps = {
color: "gray",
}

View File

@@ -0,0 +1,149 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { HTMLProps } from "react"
import LoadingDots from "src/ui/loading-dots"
type Props = {
type?: "button" | "submit" | "reset"
sizeVariant?: "input" | "small" | "medium" | "large"
/**
* variant is the visual style of the button. By default, this is a filled
* button. For a less prominent button, use minimal.
*/
variant?: Variant
/**
* intent describes the semantic meaning of the button's action. For
* dangerous or destructive actions, use danger. For actions that should
* be the primary focus, use primary.
*/
intent?: Intent
active?: boolean
/**
* prefixIcon is an icon or piece of content shown at the start of a button.
*/
prefixIcon?: React.ReactNode
/**
* suffixIcon is an icon or piece of content shown at the end of a button.
*/
suffixIcon?: React.ReactNode
/**
* loading displays a loading indicator inside the button when set to true.
* The sizing of the button is not affected by this prop.
*/
loading?: boolean
/**
* iconOnly indicates that the button contains only an icon. This is used to
* adjust styles to be appropriate for an icon-only button.
*/
iconOnly?: boolean
/**
* textAlign align the text center or left. If left aligned, any icons will
* move to the sides of the button.
*/
textAlign?: "center" | "left"
} & HTMLProps<HTMLButtonElement>
export type Variant = "filled" | "minimal"
export type Intent = "base" | "primary" | "warning" | "danger" | "black"
const Button = React.forwardRef<HTMLButtonElement, Props>((props, ref) => {
const {
className,
variant = "filled",
intent = "base",
sizeVariant = "large",
disabled,
children,
loading,
active,
iconOnly,
prefixIcon,
suffixIcon,
textAlign,
...rest
} = props
const hasIcon = Boolean(prefixIcon || suffixIcon)
return (
<button
className={cx(
"button",
{
// base filled
"bg-gray-0 border-gray-300 enabled:hover:bg-gray-100 enabled:hover:border-gray-300 enabled:hover:text-gray-900 disabled:border-gray-200 disabled:text-gray-400":
intent === "base" && variant === "filled",
"enabled:bg-gray-200 enabled:border-gray-300":
intent === "base" && variant === "filled" && active,
// primary filled
"bg-blue-500 border-blue-500 text-white enabled:hover:bg-blue-600 enabled:hover:border-blue-600 disabled:text-blue-50 disabled:bg-blue-300 disabled:border-blue-300":
intent === "primary" && variant === "filled",
// danger filled
"bg-red-400 border-red-400 text-white enabled:hover:bg-red-500 enabled:hover:border-red-500 disabled:text-red-50 disabled:bg-red-300 disabled:border-red-300":
intent === "danger" && variant === "filled",
// warning filled
"bg-yellow-300 border-yellow-300 text-white enabled:hover:bg-yellow-400 enabled:hover:border-yellow-400 disabled:text-yellow-50 disabled:bg-yellow-200 disabled:border-yellow-200":
intent === "warning" && variant === "filled",
// black filled
"bg-gray-800 border-gray-800 text-white enabled:hover:bg-gray-900 enabled:hover:border-gray-900 disabled:opacity-75":
intent === "black" && variant === "filled",
// minimal button (base variant, black is also included because its not supported for minimal buttons)
"bg-transparent border-transparent shadow-none disabled:border-transparent disabled:text-gray-400":
variant === "minimal",
"text-gray-700 enabled:focus-visible:bg-gray-100 enabled:hover:bg-gray-100 enabled:hover:text-gray-800":
variant === "minimal" && (intent === "base" || intent === "black"),
"enabled:bg-gray-200 border-gray-300":
variant === "minimal" &&
(intent === "base" || intent === "black") &&
active,
// primary minimal
"text-blue-600 enabled:focus-visible:bg-blue-0 enabled:hover:bg-blue-0 enabled:hover:text-blue-800":
variant === "minimal" && intent === "primary",
// danger minimal
"text-red-600 enabled:focus-visible:bg-red-0 enabled:hover:bg-red-0 enabled:hover:text-red-800":
variant === "minimal" && intent === "danger",
// warning minimal
"text-yellow-600 enabled:focus-visible:bg-orange-0 enabled:hover:bg-orange-0 enabled:hover:text-orange-800":
variant === "minimal" && intent === "warning",
// sizeVariants
"px-3 py-[0.35rem]": sizeVariant === "medium",
"h-input": sizeVariant === "input",
"px-3 text-sm py-[0.35rem]": sizeVariant === "small",
"button-active relative z-10": active === true,
"px-3":
iconOnly && (sizeVariant === "large" || sizeVariant === "input"),
"px-2":
iconOnly && (sizeVariant === "medium" || sizeVariant === "small"),
"icon-parent gap-2": hasIcon,
},
className
)}
ref={ref}
disabled={disabled || loading}
{...rest}
>
{prefixIcon && <span className="flex-shrink-0">{prefixIcon}</span>}
{loading && (
<LoadingDots className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-current" />
)}
{children && (
<span
className={cx({
"text-transparent": loading === true,
"text-left flex-1": textAlign === "left",
})}
>
{children}
</span>
)}
{suffixIcon && <span className="flex-shrink-0">{suffixIcon}</span>}
</button>
)
})
export default Button

View File

@@ -0,0 +1,40 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
type Props = {
children: React.ReactNode
className?: string
elevated?: boolean
empty?: boolean
noPadding?: boolean
}
/**
* Card is a box with a border, rounded corners, and some padding. Use it to
* group content into a single container and give it more importance. The
* elevation prop gives it a box shadow, while the empty prop a light gray
* background color.
*
* <Card>{content}</Card>
* <Card elevated>{content}</Card>
* <Card empty><EmptyState description="You don't have any keys" /></Card>
*
*/
export default function Card(props: Props) {
const { children, className, elevated, empty, noPadding } = props
return (
<div
className={cx("rounded-md border", className, {
"shadow-soft": elevated,
"bg-gray-0": empty,
"bg-white": !empty,
"p-6": !noPadding,
})}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import * as Primitive from "@radix-ui/react-collapsible"
import React, { useState } from "react"
import ChevronDown from "src/assets/icons/chevron-down.svg?react"
type CollapsibleProps = {
trigger?: string
children: React.ReactNode
open?: boolean
onOpenChange?: (open: boolean) => void
}
export default function Collapsible(props: CollapsibleProps) {
const { children, trigger, onOpenChange } = props
const [open, setOpen] = useState(props.open)
return (
<Primitive.Root
open={open}
onOpenChange={(open) => {
setOpen(open)
onOpenChange?.(open)
}}
>
<Primitive.Trigger className="inline-flex items-center text-gray-600 cursor-pointer hover:bg-gray-100 rounded text-sm font-medium pr-3 py-1 transition-colors">
<span className="ml-2 mr-1.5 group-hover:text-gray-500 -rotate-90 state-open:rotate-0">
<ChevronDown strokeWidth={3} className="stroke-gray-400 w-4" />
</span>
{trigger}
</Primitive.Trigger>
<Primitive.Content className="mt-2">{children}</Primitive.Content>
</Primitive.Root>
)
}

View File

@@ -0,0 +1,370 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import * as DialogPrimitive from "@radix-ui/react-dialog"
import cx from "classnames"
import React, { Component, ComponentProps, FormEvent } from "react"
import X from "src/assets/icons/x.svg?react"
import Button from "src/ui/button"
import PortalContainerContext from "src/ui/portal-container-context"
import { isObject } from "src/utils/util"
type ButtonProp = boolean | string | Partial<ComponentProps<typeof Button>>
/**
* ControlledDialogProps are common props required for dialog components with
* controlled state. Since Dialog components frequently expose these props to
* their callers, we've consolidated them here for easy access.
*/
export type ControlledDialogProps = {
/**
* open is a boolean that controls whether the dialog is open or not.
*/
open: boolean
/**
* onOpenChange is a callback that is called when the open state of the dialog
* changes.
*/
onOpenChange: (open: boolean) => void
}
type PointerDownOutsideEvent = CustomEvent<{
originalEvent: PointerEvent
}>
type Props = {
className?: string
/**
* title is the title of the dialog, shown at the top.
*/
title: string
/**
* titleSuffixDecoration is added to the title, but is not part of the ARIA label for
* the dialog. This is useful for adding a badge or other non-semantic
* information to the title.
*/
titleSuffixDecoration?: React.ReactNode
/**
* trigger is an element to use as a trigger for a dialog. Using trigger is
* preferrable to using `open` for managing state, as it allows for better
* focus management for screen readers.
*/
trigger?: React.ReactNode
/**
* children is the content of the dialog.
*/
children: React.ReactNode
/**
* defaultOpen is the default state of the dialog. This is meant to be used for
* uncontrolled dialogs, and should not be combined with `open` or
* `onOpenChange`.
*/
defaultOpen?: boolean
/**
* restoreFocus determines whether the dialog returns focus to the trigger
* element or not after closing.
*/
restoreFocus?: boolean
onPointerDownOutside?: (e: PointerDownOutsideEvent) => void
} & Partial<ControlledDialogProps>
const dialogOverlay =
"fixed overflow-y-auto inset-0 py-8 z-10 bg-gray-900 bg-opacity-[0.07]"
const dialogWindow = cx(
"bg-white rounded-lg relative max-w-lg min-w-[19rem] w-[97%] shadow-dialog",
"p-4 md:p-6 my-8 mx-auto",
// We use `transform-gpu` here to force the browser to put the dialog on its
// own layer. This helps fix some weird artifacting bugs in Safari caused by
// box-shadows. See: https://github.com/tailscale/corp/issues/12270
"transform-gpu"
)
/**
* Dialog provides a modal dialog, for prompting a user for input or confirmation
* before proceeding.
*/
export default function Dialog(props: Props) {
const {
open,
className,
defaultOpen,
onOpenChange,
trigger,
title,
titleSuffixDecoration,
children,
restoreFocus = true,
onPointerDownOutside,
} = props
return (
<DialogPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
{trigger && (
<DialogPrimitive.Trigger asChild>{trigger}</DialogPrimitive.Trigger>
)}
<PortalContainerContext.Consumer>
{(portalContainer) => (
<DialogPrimitive.Portal container={portalContainer}>
<DialogPrimitive.Overlay className={dialogOverlay}>
<DialogPrimitive.Content
aria-label={title}
className={cx(dialogWindow, className)}
onCloseAutoFocus={
// Cancel the focus restore if `restoreFocus` is set to false
restoreFocus === false ? (e) => e.preventDefault() : undefined
}
onPointerDownOutside={onPointerDownOutside}
>
<DialogErrorBoundary>
<header className="flex items-center justify-between space-x-4 mb-5 mr-8">
<div className="font-semibold text-lg truncate">
{title}
{titleSuffixDecoration}
</div>
</header>
{children}
<DialogPrimitive.Close asChild>
<Button
variant="minimal"
className="absolute top-5 right-5 px-2 py-2"
>
<X
aria-hidden
className="h-[1.25em] w-[1.25em] stroke-current"
/>
</Button>
</DialogPrimitive.Close>
</DialogErrorBoundary>
</DialogPrimitive.Content>
</DialogPrimitive.Overlay>
</DialogPrimitive.Portal>
)}
</PortalContainerContext.Consumer>
</DialogPrimitive.Root>
)
}
/**
* Dialog.Form is a standard way of providing form-based interactions in a
* Dialog component. Prefer it to custom form implementations. See each props
* documentation for details.
*
* <Dialog.Form cancelButton submitButton="Save" onSubmit={saveThing}>
* <input type="text" value={myValue} onChange={myChangeHandler} />
* </Dialog.Form>
*/
Dialog.Form = DialogForm
type FormProps = {
/**
* destructive declares whether the submit button should be styled as a danger
* button or not. Prefer `destructive` over passing a props object to
* `submitButton`, since objects cause unnecessary re-renders unless they are
* moved outside the render function.
*/
destructive?: boolean
/**
* children is the content of the dialog form.
*/
children?: React.ReactNode
/**
* disabled determines whether the submit button should be disabled. The
* cancel button cannot be disabled via this prop.
*/
disabled?: boolean
/**
* loading determines whether the submit button should display a loading state
* and the cancel button should be disabled.
*/
loading?: boolean
/**
* cancelButton determines how the cancel button looks. You can pass `true`,
* which adds a default button, pass a string which changes the button label,
* or pass an object, which is a set of props to pass to a `Button` component.
* Any unspecified props will fall back to default values.
*
* <Dialog.Form cancelButton />
* <Dialog.Form cancelButton="Done" />
* <Dialog.Form cancelButton={{ children: "Back", variant: "primary" }} />
*/
cancelButton?: ButtonProp
/**
* submitButton determines how the submit button looks. You can pass `true`,
* which adds a default button, pass a string which changes the button label,
* or pass an object, which is a set of props to pass to a `Button` component.
* Any unspecified props will fall back to default values.
*
* <Dialog.Form submitButton />
* <Dialog.Form submitButton="Save" />
* <Dialog.Form submitButton="Delete" destructive />
* <Dialog.Form submitButton={{ children: "Banana", className: "bg-yellow-500" }} />
*/
submitButton?: ButtonProp
/**
* onSubmit is the callback to use when the form is submitted. Using `onSubmit`
* is preferrable to a `onClick` handler on `submitButton`, which doesn't get
* triggered on keyboard events.
*/
onSubmit?: () => void
/**
* autoFocus makes it easy to focus a particular action button without
* overriding the button props.
*/
autoFocus?: "submit" | "cancel"
}
function DialogForm(props: FormProps) {
const {
children,
disabled = false,
destructive = false,
loading = false,
autoFocus = "submit",
cancelButton,
submitButton,
onSubmit,
} = props
const hasFooter = Boolean(cancelButton || submitButton)
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
onSubmit?.()
}
const cancelAutoFocus = Boolean(
cancelButton && !loading && autoFocus === "cancel"
)
const submitAutoFocus = Boolean(
submitButton && !loading && !disabled && autoFocus === "submit"
)
const submitIntent = destructive ? "danger" : "primary"
let cancelButtonEl = null
if (cancelButton) {
cancelButtonEl =
cancelButton === true ? (
<Button
{...cancelButtonDefaultProps}
autoFocus={cancelAutoFocus}
disabled={loading}
/>
) : typeof cancelButton === "string" ? (
<Button
{...cancelButtonDefaultProps}
autoFocus={cancelAutoFocus}
children={cancelButton}
disabled={loading}
/>
) : (
<Button
{...cancelButtonDefaultProps}
autoFocus={cancelAutoFocus}
disabled={loading}
{...cancelButton}
/>
)
const hasCustomCancelAction =
isObject(cancelButton) && cancelButton.onClick !== undefined
if (!hasCustomCancelAction) {
cancelButtonEl = (
<DialogPrimitive.Close asChild>{cancelButtonEl}</DialogPrimitive.Close>
)
}
}
return (
<form onSubmit={handleSubmit}>
{children}
{hasFooter && (
<footer className="flex mt-10 justify-end space-x-4">
{cancelButtonEl}
{submitButton && (
<>
{submitButton === true ? (
<Button
{...submitButtonDefaultProps}
intent={submitIntent}
autoFocus={submitAutoFocus}
disabled={loading || disabled}
/>
) : typeof submitButton === "string" ? (
<Button
{...submitButtonDefaultProps}
intent={submitIntent}
children={submitButton}
autoFocus={submitAutoFocus}
disabled={loading || disabled}
/>
) : (
<Button
{...submitButtonDefaultProps}
intent={submitIntent}
autoFocus={submitAutoFocus}
disabled={loading || disabled}
{...submitButton}
/>
)}
</>
)}
</footer>
)}
</form>
)
}
const cancelButtonDefaultProps: Pick<
ComponentProps<typeof Button>,
"type" | "intent" | "sizeVariant" | "children"
> = {
type: "button",
intent: "base",
sizeVariant: "medium",
children: "Cancel",
}
const submitButtonDefaultProps: Pick<
ComponentProps<typeof Button>,
"type" | "sizeVariant" | "children" | "autoFocus"
> = {
type: "submit",
sizeVariant: "medium",
children: "Submit",
}
type DialogErrorBoundaryProps = {
children: React.ReactNode
}
class DialogErrorBoundary extends Component<
DialogErrorBoundaryProps,
{ hasError: boolean }
> {
constructor(props: DialogErrorBoundaryProps) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.log(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <div className="font-semibold text-lg">Something went wrong.</div>
}
return this.props.children
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { cloneElement } from "react"
type Props = {
action?: React.ReactNode
className?: string
description: string
icon?: React.ReactElement
title?: string
}
/**
* EmptyState shows some text and an optional action when some area that can
* house content is empty (eg. no search results, empty tables).
*/
export default function EmptyState(props: Props) {
const { action, className, description, icon, title } = props
const iconColor = "text-gray-500"
const iconComponent = getIcon(icon, iconColor)
return (
<div
className={cx("flex justify-center", className, {
"flex-col items-center": action || icon || title,
})}
>
{icon && <div className="mb-2">{iconComponent}</div>}
{title && (
<h3 className="text-xl font-medium text-center mb-2">{title}</h3>
)}
<div className="w-full text-center max-w-xl text-gray-500">
{description}
</div>
{action && <div className="mt-3.5">{action}</div>}
</div>
)
}
function getIcon(icon: React.ReactElement | undefined, iconColor: string) {
return icon ? cloneElement(icon, { className: iconColor }) : null
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { InputHTMLAttributes } from "react"
type Props = {
className?: string
inputClassName?: string
error?: boolean
suffix?: JSX.Element
} & InputHTMLAttributes<HTMLInputElement>
// Input is styled in a way that only works for text inputs.
const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const {
className,
inputClassName,
error,
prefix,
suffix,
disabled,
...rest
} = props
return (
<div className={cx("relative", className)}>
<input
ref={ref}
className={cx("input z-10", inputClassName, {
"input-error": error,
})}
disabled={disabled}
{...rest}
/>
{suffix ? (
<div className="bg-white top-1 bottom-1 right-1 rounded-r-md absolute flex items-center">
{suffix}
</div>
) : null}
</div>
)
})
export default Input

View File

@@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { HTMLAttributes } from "react"
type Props = HTMLAttributes<HTMLDivElement>
/**
* LoadingDots provides a set of horizontal dots to indicate a loading state.
* These dots are helpful in horizontal contexts (like buttons) where a spinner
* doesn't fit as well.
*/
export default function LoadingDots(props: Props) {
const { className, ...rest } = props
return (
<div className={cx(className, "loading-dots")} {...rest}>
<span />
<span />
<span />
</div>
)
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import * as PopoverPrimitive from "@radix-ui/react-popover"
import cx from "classnames"
import React, { ReactNode } from "react"
import PortalContainerContext from "src/ui/portal-container-context"
type Props = {
className?: string
content: ReactNode
children: ReactNode
/**
* asChild renders the trigger element without wrapping it in a button. Use
* this when you want to use a `button` element as the trigger.
*/
asChild?: boolean
/**
* side is the side of the direction from the target element to render the
* popover.
*/
side?: "top" | "bottom" | "left" | "right"
/**
* sideOffset is how far from a give side to render the popover.
*/
sideOffset?: number
/**
* align is how to align the popover with the target element.
*/
align?: "start" | "center" | "end"
/**
* alignOffset is how far off of the alignment point to render the popover.
*/
alignOffset?: number
open?: boolean
onOpenChange?: (open: boolean) => void
}
/**
* Popover is a UI component that allows rendering unique controls in a floating
* popover, attached to a trigger element. It appears on click and manages focus
* on its own behalf.
*
* To use the Popover, pass the content as children, and give it a `trigger`:
*
* <Popover trigger={<span>Open popover</span>}>
* <p>Hello world!</p>
* </Popover>
*
* By default, the toggle is wrapped in an accessible <button> tag. You can
* customize by providing your own button and using the `asChild` prop.
*
* <Popover trigger={<Button>Hello</Button>} asChild>
* <p>Hello world!</p>
* </Popover>
*
* The former style is recommended whenever possible.
*/
export default function Popover(props: Props) {
const {
children,
className,
content,
side,
sideOffset,
align,
alignOffset,
asChild,
open,
onOpenChange,
} = props
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger asChild={asChild}>
{children}
</PopoverPrimitive.Trigger>
<PortalContainerContext.Consumer>
{(portalContainer) => (
<PopoverPrimitive.Portal container={portalContainer}>
<PopoverPrimitive.Content
className={cx(
"origin-radix-popover shadow-popover bg-white rounded-md z-50",
"state-open:animate-scale-in state-closed:animate-scale-out",
className
)}
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
collisionPadding={12}
>
{content}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
)}
</PortalContainerContext.Consumer>
</PopoverPrimitive.Root>
)
}
Popover.defaultProps = {
sideOffset: 10,
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
const PortalContainerContext = React.createContext<HTMLElement | undefined>(
undefined
)
export default PortalContainerContext

View File

@@ -0,0 +1,41 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React from "react"
export default function ProfilePic({
url,
size = "large",
className,
}: {
url?: string
size?: "small" | "medium" | "large"
className?: string
}) {
return (
<div
className={cx(
"relative flex-shrink-0 rounded-full overflow-hidden",
{
"w-5 h-5": size === "small",
"w-[26px] h-[26px]": size === "medium",
"w-8 h-8": size === "large",
},
className
)}
>
{url ? (
<div
className="w-full h-full flex pointer-events-none rounded-full bg-gray-200"
style={{
backgroundImage: `url(${url})`,
backgroundSize: "cover",
}}
/>
) : (
<div className="w-full h-full flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
)}
</div>
)
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useEffect, useRef, useState } from "react"
import useToaster from "src/hooks/toaster"
import { copyText } from "src/utils/clipboard"
type Props = {
className?: string
hideAffordance?: boolean
/**
* primaryActionSubject is the subject of the toast confirmation message
* "Copied <subject> to clipboard"
*/
primaryActionSubject: string
primaryActionValue: string
secondaryActionName?: string
secondaryActionValue?: string
/**
* secondaryActionSubject is the subject of the toast confirmation message
* prompted by the secondary action "Copied <subject> to clipboard"
*/
secondaryActionSubject?: string
children?: React.ReactNode
/**
* onSecondaryAction is used to trigger events when the secondary copy
* function is used. It is not used when the secondary action is hidden.
*/
onSecondaryAction?: () => void
}
/**
* QuickCopy is a UI component that allows for copying textual content in one click.
*/
export default function QuickCopy(props: Props) {
const {
className,
hideAffordance,
primaryActionSubject,
primaryActionValue,
secondaryActionValue,
secondaryActionName,
secondaryActionSubject,
onSecondaryAction,
children,
} = props
const toaster = useToaster()
const containerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLDivElement>(null)
const [showButton, setShowButton] = useState(false)
useEffect(() => {
if (!showButton) {
return
}
if (!containerRef.current || !buttonRef.current) {
return
}
// We don't need to watch any `resize` event because it's pretty unlikely
// the browser will resize while their cursor is over one of these items.
const rect = containerRef.current.getBoundingClientRect()
const maximumPossibleWidth = window.innerWidth - rect.left + 4
// We add the border-width (1px * 2 sides) and the padding (0.5rem * 2 sides)
// and add 1px for rounding up the calculation in order to get the final
// maxWidth value. This should be kept in sync with the CSS classes below.
buttonRef.current.style.maxWidth = `${maximumPossibleWidth}px`
buttonRef.current.style.visibility = "visible"
}, [showButton])
const handlePrimaryAction = () => {
copyText(primaryActionValue)
toaster.show({
message: `Copied ${primaryActionSubject} to the clipboard`,
})
}
const handleSecondaryAction = () => {
if (!secondaryActionValue) {
return
}
copyText(secondaryActionValue)
toaster.show({
message: `Copied ${
secondaryActionSubject || secondaryActionName
} to the clipboard`,
})
onSecondaryAction?.()
}
return (
<div
className="flex relative min-w-0"
ref={containerRef}
// Since the affordance is a child of this element, we assign both event
// handlers here.
onMouseLeave={() => setShowButton(false)}
>
<div
onMouseEnter={() => setShowButton(true)}
className={cx("truncate", className)}
>
{children}
</div>
{!hideAffordance && (
<button
onMouseEnter={() => setShowButton(true)}
onClick={handlePrimaryAction}
className={cx("cursor-pointer text-blue-500", { "ml-2": children })}
>
Copy
</button>
)}
{showButton && (
<div
className="absolute -mt-1 -ml-2 -top-px -left-px
shadow-md cursor-pointer rounded-md active:shadow-sm
transition-shadow duration-100 ease-in-out z-50"
style={{ visibility: "hidden" }}
ref={buttonRef}
>
<div className="flex border rounded-md button-outline bg-white">
<div
className={cx("flex min-w-0 py-1 px-2 hover:bg-gray-0", {
"rounded-md": !secondaryActionValue,
"rounded-l-md": secondaryActionValue,
})}
onClick={handlePrimaryAction}
>
<span
className={cx(className, "inline-block select-none truncate")}
>
{children}
</span>
<button
className={cx("cursor-pointer text-blue-500", {
"ml-2": children,
})}
>
Copy
</button>
</div>
{secondaryActionValue && (
<div
className="text-blue-500 py-1 px-2 border-l hover:bg-gray-100 rounded-r-md"
onClick={handleSecondaryAction}
>
{secondaryActionName}
</div>
)}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { forwardRef, InputHTMLAttributes } from "react"
import Search from "src/assets/icons/search.svg?react"
type Props = {
className?: string
inputClassName?: string
} & InputHTMLAttributes<HTMLInputElement>
/**
* SearchInput is a standard input with a search icon.
*/
const SearchInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, inputClassName, ...rest } = props
return (
<div className={cx("relative", className)}>
<Search className="absolute text-gray-400 w-[1.25em] h-full ml-2" />
<input
type="text"
className={cx("input pl-9 pr-8", inputClassName)}
ref={ref}
{...rest}
/>
</div>
)
})
SearchInput.displayName = "SearchInput"
export default SearchInput

View File

@@ -0,0 +1,32 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { HTMLAttributes } from "react"
type Props = {
className?: string
size: "sm" | "md"
} & HTMLAttributes<HTMLDivElement>
export default function Spinner(props: Props) {
const { className, size, ...rest } = props
return (
<div
className={cx(
"spinner inline-block rounded-full align-middle",
{
"border-2 w-4 h-4": size === "sm",
"border-4 w-8 h-8": size === "md",
},
className
)}
{...rest}
/>
)
}
Spinner.defaultProps = {
size: "md",
}

View File

@@ -0,0 +1,280 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useState,
} from "react"
import { createPortal } from "react-dom"
import X from "src/assets/icons/x.svg?react"
import { noop } from "src/utils/util"
import { create } from "zustand"
import { shallow } from "zustand/shallow"
// Set up root element on the document body for toasts to render into.
const root = document.createElement("div")
root.id = "toast-root"
root.classList.add("relative", "z-20")
document.body.append(root)
const toastSpacing = remToPixels(1)
export type Toaster = {
clear: () => void
dismiss: (key: string) => void
show: (props: Toast) => string
}
type Toast = {
key?: string // key is a unique string value that ensures only one toast with a given key is shown at a time.
className?: string
variant?: "danger" // styling for the toast, undefined is neutral, danger is for failed requests
message: React.ReactNode
timeout?: number
added?: number // timestamp of when the toast was added
}
type ToastWithKey = Toast & { key: string }
type State = {
toasts: ToastWithKey[]
maxToasts: number
clear: () => void
dismiss: (key: string) => void
show: (props: Toast) => string
}
const useToasterState = create<State>((set, get) => ({
toasts: [],
maxToasts: 5,
clear: () => {
set({ toasts: [] })
},
dismiss: (key: string) => {
set((prev) => ({
toasts: prev.toasts.filter((t) => t.key !== key),
}))
},
show: (props: Toast) => {
const { toasts: prevToasts, maxToasts } = get()
const propsWithKey = {
key: Date.now().toString(),
...props,
}
const prevIdx = prevToasts.findIndex((t) => t.key === propsWithKey.key)
// If the toast already exists, update it. Otherwise, append it.
const nextToasts =
prevIdx !== -1
? [
...prevToasts.slice(0, prevIdx),
propsWithKey,
...prevToasts.slice(prevIdx + 1),
]
: [...prevToasts, propsWithKey]
set({
// Get the last `maxToasts` toasts of the set.
toasts: nextToasts.slice(-maxToasts),
})
return propsWithKey.key
},
}))
const clearSelector = (state: State) => state.clear
const toasterSelector = (state: State) => ({
show: state.show,
dismiss: state.dismiss,
clear: state.clear,
})
/**
* useRawToasterForHook is meant to supply the hook function for hooks/toaster.
* Use hooks/toaster instead.
*/
export const useRawToasterForHook = () =>
useToasterState(toasterSelector, shallow)
type ToastProviderProps = {
children: React.ReactNode
canEscapeKeyClear?: boolean
}
/**
* ToastProvider is the top-level toaster component. It stores the toast state.
*/
export default function ToastProvider(props: ToastProviderProps) {
const { children, canEscapeKeyClear = true } = props
const clear = useToasterState(clearSelector)
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (!canEscapeKeyClear) {
return
}
if (e.key === "Esc" || e.key === "Escape") {
clear()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [canEscapeKeyClear, clear])
return (
<>
{children}
<ToastContainer />
</>
)
}
const toastContainerSelector = (state: State) => ({
toasts: state.toasts,
dismiss: state.dismiss,
})
/**
* ToastContainer manages the positioning and animation for all currently
* displayed toasts. It should only be used by ToastProvider.
*/
function ToastContainer() {
const { toasts, dismiss } = useToasterState(toastContainerSelector, shallow)
const [prevToasts, setPrevToasts] = useState<ToastWithKey[]>(toasts)
useEffect(() => setPrevToasts(toasts), [toasts])
const [refMap] = useState(() => new Map<string, HTMLDivElement>())
const getOffsetForToast = useCallback(
(key: string) => {
let offset = 0
let arr = toasts
let index = arr.findIndex((t) => t.key === key)
if (index === -1) {
arr = prevToasts
index = arr.findIndex((t) => t.key === key)
}
if (index === -1) {
return offset
}
for (let i = arr.length; i > index; i--) {
if (!arr[i]) {
continue
}
const ref = refMap.get(arr[i].key)
if (!ref) {
continue
}
offset -= ref.offsetHeight
offset -= toastSpacing
}
return offset
},
[refMap, prevToasts, toasts]
)
const toastsWithStyles = useMemo(
() =>
toasts.map((toast) => ({
toast: toast,
style: {
transform: `translateY(${getOffsetForToast(toast.key)}px) scale(1.0)`,
},
})),
[getOffsetForToast, toasts]
)
if (!root) {
throw new Error("Could not find toast root") // should never happen
}
return createPortal(
<div className="fixed bottom-6 right-6 z-[99]">
{toastsWithStyles.map(({ toast, style }) => (
<ToastBlock
key={toast.key}
ref={(ref) => ref && refMap.set(toast.key, ref)}
toast={toast}
onDismiss={dismiss}
style={style}
/>
))}
</div>,
root
)
}
/**
* ToastBlock is the display of an individual toast, and also manages timeout
* settings for a particular toast.
*/
const ToastBlock = forwardRef<
HTMLDivElement,
{
toast: ToastWithKey
onDismiss?: (key: string) => void
style?: React.CSSProperties
}
>(({ toast, onDismiss = noop, style }, ref) => {
const { message, key, timeout = 5000, variant } = toast
const [focused, setFocused] = useState(false)
const dismiss = useCallback(() => onDismiss(key), [onDismiss, key])
const onFocus = useCallback(() => setFocused(true), [])
const onBlur = useCallback(() => setFocused(false), [])
useEffect(() => {
if (timeout <= 0 || focused) {
return
}
const timerId = setTimeout(() => dismiss(), timeout)
return () => clearTimeout(timerId)
}, [dismiss, timeout, focused])
return (
<div
className={cx(
"transition ease-in-out animate-scale-in",
"bottom-0 right-0 z-[99] w-[85vw] origin-bottom",
"sm:min-w-[400px] sm:max-w-[500px]",
"absolute shadow-sm rounded-md text-md flex items-center justify-between",
{
"text-white bg-gray-700": variant === undefined,
"text-white bg-orange-400": variant === "danger",
}
)}
aria-live="polite"
ref={ref}
onBlur={onBlur}
onFocus={onFocus}
onMouseEnter={onFocus}
onMouseLeave={onBlur}
tabIndex={0}
style={style}
>
<span className="pl-4 py-3 pr-2">{message}</span>
<button
className="cursor-pointer opacity-75 hover:opacity-50 transition-opacity py-3 px-3"
onClick={dismiss}
>
<X className="w-[1em] h-[1em] stroke-current" />
</button>
</div>
)
})
function remToPixels(rem: number) {
return (
rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize)
)
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { ChangeEvent } from "react"
type Props = {
id?: string
className?: string
disabled?: boolean
checked: boolean
sizeVariant?: "small" | "medium" | "large"
onChange: (checked: boolean) => void
}
export default function Toggle(props: Props) {
const { className, id, disabled, checked, sizeVariant, onChange } = props
function handleChange(e: ChangeEvent<HTMLInputElement>) {
onChange(e.target.checked)
}
return (
<input
id={id}
type="checkbox"
className={cx(
"toggle",
{
"toggle-large": sizeVariant === "large",
"toggle-small": sizeVariant === "small",
},
className
)}
disabled={disabled}
checked={checked}
onChange={handleChange}
/>
)
}
Toggle.defaultProps = {
sizeVariant: "medium",
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { isPromise } from "src/utils/util"
/**
* copyText copies text to the clipboard, handling cross-browser compatibility
* issues with different clipboard APIs.
*
* To support copying after running a network request (eg. generating an invite),
* pass a promise that resolves to the text to copy.
*
* @example
* copyText("Hello, world!")
* copyText(generateInvite().then(res => res.data.inviteCode))
*/
export function copyText(text: string | Promise<string | void>) {
if (!navigator.clipboard) {
if (isPromise(text)) {
return text.then((val) => fallbackCopy(validateString(val)))
}
return fallbackCopy(text)
}
if (isPromise(text)) {
if (typeof ClipboardItem === "undefined") {
return text.then((val) =>
navigator.clipboard.writeText(validateString(val))
)
}
return navigator.clipboard.write([
new ClipboardItem({
"text/plain": text.then(
(val) => new Blob([validateString(val)], { type: "text/plain" })
),
}),
])
}
return navigator.clipboard.writeText(text)
}
function validateString(val: unknown): string {
if (typeof val !== "string" || val.length === 0) {
throw new TypeError("Expected string, got " + typeof val)
}
if (val.length === 0) {
throw new TypeError("Expected non-empty string")
}
return val
}
function fallbackCopy(text: string) {
const el = document.createElement("textarea")
el.value = text
el.setAttribute("readonly", "")
el.className = "absolute opacity-0 pointer-events-none"
document.body.append(el)
// Check if text is currently selected
let selection = document.getSelection()
const selected =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : false
el.select()
document.execCommand("copy")
el.remove()
// Restore selection
if (selected) {
selection = document.getSelection()
if (selection) {
selection.removeAllRanges()
selection.addRange(selected)
}
}
return Promise.resolve()
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { isTailscaleIPv6, pluralize } from "src/utils/util"
import { describe, expect, it } from "vitest"
describe("pluralize", () => {
it("test routes", () => {
expect(pluralize("route", "routes", 1)).toBe("route")
expect(pluralize("route", "routes", 2)).toBe("routes")
})
})
describe("isTailscaleIPv6", () => {
it("test ips", () => {
expect(isTailscaleIPv6("100.101.102.103")).toBeFalsy()
expect(
isTailscaleIPv6("fd7a:115c:a1e0:ab11:1111:cd11:111e:f11g")
).toBeTruthy()
})
})

View File

@@ -0,0 +1,58 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
/**
* assertNever ensures a branch of code can never be reached,
* resulting in a Typescript error if it can.
*/
export function assertNever(a: never): never {
return a
}
/**
* noop is an empty function for use as a default value.
*/
export function noop() {}
/**
* isObject checks if a value is an object.
*/
export function isObject(val: unknown): val is object {
return Boolean(val && typeof val === "object" && val.constructor === Object)
}
/**
* pluralize is a very simple function that returns either
* the singular or plural form of a string based on the given
* quantity.
*
* TODO: Ideally this would use a localized pluralization.
*/
export function pluralize(signular: string, plural: string, qty: number) {
return qty === 1 ? signular : plural
}
/**
* isTailscaleIPv6 returns true when the ip matches
* Tailnet's IPv6 format.
*/
export function isTailscaleIPv6(ip: string): boolean {
return ip.startsWith("fd7a:115c:a1e0")
}
/**
* isPromise returns whether the current value is a promise.
*/
export function isPromise<T = unknown>(val: unknown): val is Promise<T> {
if (!val) {
return false
}
return typeof val === "object" && "then" in val
}
/**
* isHTTPS reports whether the current page is loaded over HTTPS.
*/
export function isHTTPS() {
return window.location.protocol === "https:"
}

85
client/web/styles.json Normal file
View File

@@ -0,0 +1,85 @@
{
"colors": {
"transparent": "transparent",
"current": "currentColor",
"white": "rgb(var(--color-white) / <alpha-value>)",
"gray": {
"0": "rgb(var(--color-gray-0) / <alpha-value>)",
"50": "rgb(var(--color-gray-50) / <alpha-value>)",
"100": "rgb(var(--color-gray-100) / <alpha-value>)",
"200": "rgb(var(--color-gray-200) / <alpha-value>)",
"300": "rgb(var(--color-gray-300) / <alpha-value>)",
"400": "rgb(var(--color-gray-400) / <alpha-value>)",
"500": "rgb(var(--color-gray-500) / <alpha-value>)",
"600": "rgb(var(--color-gray-600) / <alpha-value>)",
"700": "rgb(var(--color-gray-700) / <alpha-value>)",
"800": "rgb(var(--color-gray-800) / <alpha-value>)",
"900": "rgb(var(--color-gray-900) / <alpha-value>)"
},
"blue": {
"0": "rgb(var(--color-blue-0) / <alpha-value>)",
"50": "rgb(var(--color-blue-50) / <alpha-value>)",
"100": "rgb(var(--color-blue-100) / <alpha-value>)",
"200": "rgb(var(--color-blue-200) / <alpha-value>)",
"300": "rgb(var(--color-blue-300) / <alpha-value>)",
"400": "rgb(var(--color-blue-400) / <alpha-value>)",
"500": "rgb(var(--color-blue-500) / <alpha-value>)",
"600": "rgb(var(--color-blue-600) / <alpha-value>)",
"700": "rgb(var(--color-blue-700) / <alpha-value>)",
"800": "rgb(var(--color-blue-800) / <alpha-value>)",
"900": "rgb(var(--color-blue-900) / <alpha-value>)"
},
"green": {
"0": "rgb(var(--color-green-0) / <alpha-value>)",
"50": "rgb(var(--color-green-50) / <alpha-value>)",
"100": "rgb(var(--color-green-100) / <alpha-value>)",
"200": "rgb(var(--color-green-200) / <alpha-value>)",
"300": "rgb(var(--color-green-300) / <alpha-value>)",
"400": "rgb(var(--color-green-400) / <alpha-value>)",
"500": "rgb(var(--color-green-500) / <alpha-value>)",
"600": "rgb(var(--color-green-600) / <alpha-value>)",
"700": "rgb(var(--color-green-700) / <alpha-value>)",
"800": "rgb(var(--color-green-800) / <alpha-value>)",
"900": "rgb(var(--color-green-900) / <alpha-value>)"
},
"red": {
"0": "rgb(var(--color-red-0) / <alpha-value>)",
"50": "rgb(var(--color-red-50) / <alpha-value>)",
"100": "rgb(var(--color-red-100) / <alpha-value>)",
"200": "rgb(var(--color-red-200) / <alpha-value>)",
"300": "rgb(var(--color-red-300) / <alpha-value>)",
"400": "rgb(var(--color-red-400) / <alpha-value>)",
"500": "rgb(var(--color-red-500) / <alpha-value>)",
"600": "rgb(var(--color-red-600) / <alpha-value>)",
"700": "rgb(var(--color-red-700) / <alpha-value>)",
"800": "rgb(var(--color-red-800) / <alpha-value>)",
"900": "rgb(var(--color-red-900) / <alpha-value>)"
},
"yellow": {
"0": "rgb(var(--color-yellow-0) / <alpha-value>)",
"50": "rgb(var(--color-yellow-50) / <alpha-value>)",
"100": "rgb(var(--color-yellow-100) / <alpha-value>)",
"200": "rgb(var(--color-yellow-200) / <alpha-value>)",
"300": "rgb(var(--color-yellow-300) / <alpha-value>)",
"400": "rgb(var(--color-yellow-400) / <alpha-value>)",
"500": "rgb(var(--color-yellow-500) / <alpha-value>)",
"600": "rgb(var(--color-yellow-600) / <alpha-value>)",
"700": "rgb(var(--color-yellow-700) / <alpha-value>)",
"800": "rgb(var(--color-yellow-800) / <alpha-value>)",
"900": "rgb(var(--color-yellow-900) / <alpha-value>)"
},
"orange": {
"0": "rgb(var(--color-orange-0) / <alpha-value>)",
"50": "rgb(var(--color-orange-50) / <alpha-value>)",
"100": "rgb(var(--color-orange-100) / <alpha-value>)",
"200": "rgb(var(--color-orange-200) / <alpha-value>)",
"300": "rgb(var(--color-orange-300) / <alpha-value>)",
"400": "rgb(var(--color-orange-400) / <alpha-value>)",
"500": "rgb(var(--color-orange-500) / <alpha-value>)",
"600": "rgb(var(--color-orange-600) / <alpha-value>)",
"700": "rgb(var(--color-orange-700) / <alpha-value>)",
"800": "rgb(var(--color-orange-800) / <alpha-value>)",
"900": "rgb(var(--color-orange-900) / <alpha-value>)"
}
}
}

View File

@@ -7,6 +7,7 @@
package web
import (
"errors"
"fmt"
"net/http"
"os/exec"
@@ -17,62 +18,42 @@ import (
// authorizeSynology authenticates the logged-in Synology user and verifies
// that they are authorized to use the web client.
// It reports true if the request is authorized to continue, and false otherwise.
// authorizeSynology manages writing out any relevant authorization errors to the
// ResponseWriter itself.
func authorizeSynology(w http.ResponseWriter, r *http.Request) (ok bool) {
if synoTokenRedirect(w, r) {
return false
// If the user is authenticated, but not authorized to use the client, an error is returned.
func authorizeSynology(r *http.Request) (authorized bool, err error) {
if !hasSynoToken(r) {
return false, nil
}
// authenticate the Synology user
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, fmt.Sprintf("auth: %v: %s", err, out), http.StatusUnauthorized)
return false
return false, fmt.Errorf("auth: %v: %s", err, out)
}
user := strings.TrimSpace(string(out))
// check if the user is in the administrators group
isAdmin, err := groupmember.IsMemberOfGroup("administrators", user)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return false
return false, err
}
if !isAdmin {
http.Error(w, "not a member of administrators group", http.StatusForbidden)
return false
return false, errors.New("not a member of administrators group")
}
return true
return true, nil
}
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
// hasSynoToken returns true if the request include a SynoToken used for synology auth.
func hasSynoToken(r *http.Request) bool {
if r.Header.Get("X-Syno-Token") != "" {
return false
return true
}
if r.URL.Query().Get("SynoToken") != "" {
return false
return true
}
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
return false
return true
}
// We need a SynoToken for authenticate.cgi.
// So we tell the client to get one.
_, _ = fmt.Fprint(w, synoTokenRedirectHTML)
return true
return false
}
const synoTokenRedirectHTML = `<html>
Redirecting with session token...
<script>
fetch("/webman/login.cgi")
.then(r => r.json())
.then(data => {
u = new URL(window.location)
u.searchParams.set("SynoToken", data.SynoToken)
document.location = u
})
</script>
`

View File

@@ -1,12 +1,117 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
import plugin from "tailwindcss/plugin"
import styles from "./styles.json"
const config = {
theme: {
extend: {},
screens: {
sm: "420px",
md: "768px",
lg: "1024px",
},
fontFamily: {
sans: [
"Inter",
"-apple-system",
"BlinkMacSystemFont",
"Helvetica",
"Arial",
"sans-serif",
],
mono: [
"SFMono-Regular",
"SFMono Regular",
"Consolas",
"Liberation Mono",
"Menlo",
"Courier",
"monospace",
],
},
fontWeight: {
normal: "400",
medium: "500",
semibold: "600",
bold: "700",
},
colors: styles.colors,
extend: {
colors: {
...styles.colors,
"bg-app": "var(--color-bg-app)",
"bg-menu-item-hover": "var(--color-bg-menu-item-hover)",
"border-base": "var(--color-border-base)",
"text-base": "var(--color-text-base)",
"text-muted": "var(--color-text-muted)",
"text-disabled": "var(--color-text-disabled)",
"text-primary": "var(--color-text-primary)",
"text-warning": "var(--color-text-warning)",
"text-danger": "var(--color-text-danger)",
},
borderColor: {
DEFAULT: "var(--color-border-base)",
},
boxShadow: {
dialog: "0 10px 40px rgba(0,0,0,0.12), 0 0 16px rgba(0,0,0,0.08)",
form: "0 1px 1px rgba(0, 0, 0, 0.04)",
soft: "0 4px 12px 0 rgba(0, 0, 0, 0.03)",
popover:
"0 0 0 1px rgba(136, 152, 170, 0.1), 0 15px 35px 0 rgba(49, 49, 93, 0.1), 0 5px 15px 0 rgba(0, 0, 0, 0.08)",
},
animation: {
"scale-in": "scale-in 120ms cubic-bezier(0.16, 1, 0.3, 1)",
"scale-out": "scale-out 120ms cubic-bezier(0.16, 1, 0.3, 1)",
},
transformOrigin: {
"radix-hovercard": "var(--radix-hover-card-content-transform-origin)",
"radix-popover": "var(--radix-popover-content-transform-origin)",
"radix-tooltip": "var(--radix-tooltip-content-transform-origin)",
},
keyframes: {
"scale-in": {
"0%": {
transform: "scale(0.94)",
opacity: "0",
},
"100%": {
transform: "scale(1)",
opacity: "1",
},
},
"scale-out": {
"0%": {
transform: "scale(1)",
opacity: "1",
},
"100%": {
transform: "scale(0.94)",
opacity: "0",
},
},
},
},
},
plugins: [],
plugins: [
plugin(function ({ addVariant }) {
addVariant("state-open", [
"&[data-state=“open”]",
"[data-state=“open”] &",
])
addVariant("state-closed", [
"&[data-state=“closed”]",
"[data-state=“closed”] &",
])
addVariant("state-delayed-open", [
"&[data-state=“delayed-open”]",
"[data-state=“delayed-open”] &",
])
addVariant("state-active", ["&[data-state=“active”]"])
addVariant("state-inactive", ["&[data-state=“inactive”]"])
}),
],
content: ["./src/**/*.html", "./src/**/*.{ts,tsx}", "./index.html"],
}
export default config

View File

@@ -5,6 +5,7 @@
"module": "ES2020",
"strict": true,
"sourceMap": true,
"skipLibCheck": true,
"isolatedModules": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,

Some files were not shown because too many files have changed in this diff Show More