Compare commits

...

740 Commits

Author SHA1 Message Date
License Updater
c684ca7a0c licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-01-09 14:36:00 -08:00
Brad Fitzpatrick
1116602d4c ssh/tailssh: add OpenBSD support for Tailscale SSH
And bump go.mod for https://github.com/u-root/u-root/pull/2593

Change-Id: I36ec94c5b2b76d671cb739f1e9a1a43ca1d9d1b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-09 12:58:15 -08:00
Brad Fitzpatrick
be67b8e75b ssh/tailssh: fix Tailscale SSH to non-root tailscaled
Fix regression from 337c77964b where
tailscaled started calling Setgroups. Prior to that, SSH to a non-root
tailscaled was working.

Instead, ignore any failure calling Setgroups if the groups are
already correct.

Fixes #6888

Change-Id: I561991ddb37eaf2620759c6bcaabd36e0fb2a22d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-06 13:19:12 -08:00
Brad Fitzpatrick
8047dfa2dc ssh/tailssh: unify some of the incubator_* GOOS files into incubator.go
In prep for fix for #6888

Change-Id: I79f780c6467a9b7ac03017b27d412d6b0d2f7e6b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-06 13:19:12 -08:00
Brad Fitzpatrick
ebbf5c57b3 README.md: update with some new links, refresh
And remove Darwin from the list, as macOS was already there.

Change-Id: I76bdcad97c926771f44a67140af21f07a8334796
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-05 13:06:45 -08:00
David Anderson
39efba528f cmd/containerboot: use TS_AUTHKEY as the parameter for auth keys
We still accept the previous TS_AUTH_KEY for backwards compatibility, but the documented option name is the spelling we use everywhere else.

Updates #6321

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-01-05 13:03:39 -08:00
Brad Fitzpatrick
69c0b7e712 ipn/ipnlocal: add c2n handler to flush logtail for support debugging
Updates tailscale/corp#8564

Change-Id: I0c619d4007069f90cffd319fba66bd034d63e84d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-05 12:06:07 -08:00
Tom DNetto
673b3d8dbd net/dns,userspace: remove unused DNS paths, normalize query limit on iOS
With a42a594bb3, iOS uses netstack and
hence there are no longer any platforms which use the legacy MagicDNS path. As such, we remove it.

We also normalize the limit for max in-flight DNS queries on iOS (it was 64, now its 256 as per other platforms).
It was 64 for the sake of being cautious about memory, but now we have 50Mb (iOS-15 and greater) instead of 15Mb
so we have the spare headroom.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-01-05 11:56:14 -08:00
Brad Fitzpatrick
10eec37cd9 scripts: permit 2023 in license headers
Change-Id: Ia018cb8491871c8bf756c454d085780b75512962
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-05 11:41:47 -08:00
Mihai Parparita
8f2bc0708b logtail: make logs flush delay dynamic
Instead of a static FlushDelay configuration value, use a FlushDelayFn
function that we invoke every time we decide send logs. This will allow
mobile clients to be more dynamic about when to send logs.

Updates #6768

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-01-04 16:59:25 -08:00
Tom DNetto
0088c5ddc0 health,ipn/ipnlocal: report the node being locked out as a health issue
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-01-04 16:20:47 -08:00
Tom DNetto
907f85cd67 cmd/tailscale,tka: make KeyID return an error instead of panicking
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-01-04 09:51:31 -08:00
Tom DNetto
8724aa254f cmd/tailscale,tka: implement compat for TKA messages, minor UX tweaks
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-01-04 09:51:31 -08:00
Kristoffer Dalby
c4e262a0fc ipn/profiles: set default prefs based on Windows registry (#6803)
Co-authored-by: Maisem Ali maisem@tailscale.com
2023-01-04 18:34:31 +01:00
Brad Fitzpatrick
eafbf8886d ipn/localapi: add localapi debug endpoints for packet filter/matches
For debugging #6423. This is easier than TS_DEBUG_MAP, as this means I
can pipe things into jq, etc.

Updates #6423

Change-Id: Ib3e7496b2eb3f47d4bed42e9b8045a441424b23c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-03 15:54:51 -08:00
Brad Fitzpatrick
b2b8e62476 util/codegen: permit running in directories without copyright headers
It broke in our corp repo that lacks copyright headers.

Change-Id: Iafc433e6b6affe83b45477899455527658dc4f12
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-03 11:09:35 -08:00
David Anderson
91e64ca74f cmd/tailscale/cli: redact private key in debug netmap output by default
This makes `tailscale debug watch-ipn` safe to use for troubleshooting
user issues, in addition to local debugging during development.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-01-03 10:06:24 -08:00
License Updater
d72575eaaa licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-01-03 09:48:39 -08:00
James Tucker
b2c55e62c8 net/tlsdial,tstest,version: use go command from $PATH
Go now includes the GOROOT bin directory in $PATH while running tests
and generate, so it is no longer necessary to construct a path using
runtime.GOROOT().

Fixes #6689

Signed-off-by: James Tucker <james@tailscale.com>
2023-01-03 09:30:23 -08:00
Denton Gentry
467ace7d0c cmd/tailscale: use localhost for QNAP authLogin.cgi
When the user clicks on the Tailscale app in the QNAP App Center,
we do a GET from /cgi-bin/authLogin.cgi to look up their SID.

If the user clicked "secure login" on the QNAP login page to use
HTTPS, then our access to authLogin.cgi will also use HTTPS
but the certiciate is self-signed. Our GET fails with:
    Get "https://10.1.10.41/cgi-bin/authLogin.cgi?sid=abcd0123":
    x509: cannot validate certificate for 10.1.10.41 because it
    doesn't contain any IP SANs
or similar errors.

Instead, access QNAP authentication via http://localhost:8080/
as documented in
https://download.qnap.com/dev/API_QNAP_QTS_Authentication.pdf

Fixes https://github.com/tailscale/tailscale-qpkg/issues/62

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-01-03 06:09:10 -08:00
Brad Fitzpatrick
aad6830df0 util/codegen, all: use latest year, not time.Now, in generated files
Updates #6865

Change-Id: I6b86c646968ebbd4553cf37df5e5612fbf5c5f7d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-02 20:38:32 -08:00
Brad Fitzpatrick
ea70aa3d98 net/dns/resolvconffile: fix handling of multiple search domains
Fixes #6875

Change-Id: I57eb9312c9a1c81792ce2b5a0a0f254213b05df2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-02 20:19:16 -08:00
License Updater
692eac23ad licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-29 21:01:02 -08:00
David Anderson
c86d9f2ab1 flake.nix: rename package to just "tailscale".
There is no unstability inherent in this package, it's just
unstable if you choose to import the flake at the main branch.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-24 18:17:24 -08:00
David Anderson
7bfb9999ee cmd/printdep: support printing the toolchain SRI hash.
Updates #6845.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-24 15:40:22 -08:00
David Anderson
d2beaea523 update-flake.sh: tooling to keep Nix SRI hashes in sync.
Also fixes the Go toolchain SRI hash from a7f05c6bb0,
it turns out I initialized the file with an SRI hash for an older
toolchain version, and because of the unique way fixed-output derivations
work in nix, nix didn't tell me about the mismatch because it just
cache-hit on the older toolchain and moved on. Sigh.

Updates #6845.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-24 15:22:41 -08:00
Brad Fitzpatrick
3599364312 cmd/nardump: Go tool to build Nix NARs and compute their hashes.
Updates #6845.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-24 15:22:41 -08:00
David Anderson
a7f05c6bb0 flake.nix: init to ship unstable tailscale packages.
With this, you can import "github:tailscale/tailscale" as a nix flake,
and get access to the "tailscale-unstable" package.

Updates #6845.

Signed-off-by: David Anderson <dave@natulte.net>
2022-12-24 14:49:03 -08:00
David Anderson
eb682d2a0b version: construct short hash in dev mode if GitCommit is given.
Allows a dev built to provide GitCommit and have the short hash
computed correctly, even if the Go embedded build info lacks a
git commit.

Signed-off-by: David Anderson <dave@natulte.net>
2022-12-24 14:49:03 -08:00
Denton Gentry
2a1f1c79ca scripts/installer.sh: add SUSE Enterprise Server.
Fixes https://github.com/tailscale/tailscale/issues/6840

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-12-24 08:12:01 -08:00
License Updater
6107c65f1e licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-23 14:54:26 -08:00
Claire Wang
a45c9f982a wgengine/netstack: change netstack API to require LocalBackend
The macOS client was forgetting to call netstack.Impl.SetLocalBackend.
Change the API so that it can't be started without one, eliminating this
class of bug. Then update all the callers.

Updates #6764

Change-Id: I2b3a4f31fdfd9fdbbbbfe25a42db0c505373562f
Signed-off-by: Claire Wang <claire@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-23 14:01:26 -08:00
Brad Fitzpatrick
84eaef0bbb ssh/tailssh: don't swallow process exit code in be-child
Thanks to @nshalman and @Soypete for debugging!

Updates #6054

Change-Id: I74550cc31f8a257b37351b8152634c768e1e0a8a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-23 12:56:10 -08:00
License Updater
f3c83a06ff licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-22 15:46:14 -08:00
License Updater
011f661d5b licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-22 15:45:53 -08:00
Brad Fitzpatrick
caa2fe394f wgengine/netstack: delete some dead code, old comment, use atomic int types
Noticed while looking at something else; #cleanup.

Change-Id: Icde7749363014eab9bebe1dd80708f5491f933d1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-22 13:04:21 -08:00
Anton Tolchanov
82b9689e25 ipn/ipnlocal: maintain a proxy handler per backend (#6804)
By default, `http.Transport` keeps idle connections open hoping to re-use them in the future. Combined with a separate transport per request in HTTP proxy this results in idle connection leak.

Fixes #6773
2022-12-21 18:36:58 +00:00
Andrew Dunham
1011e64ad7 wgengine/monitor: don't log unhandled RTM_{NEW,DEL}LINK messages
These aren't handled, but it's not an error to get one.

Fixes #6806

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I1fcb9032ac36420aa72a048bf26f58360b9461f9
2022-12-21 12:11:45 -05:00
Brad Fitzpatrick
be10b529ec wgengine/magicsock: add TS_DISCO_PONG_IPV4_DELAY knob to bias IPv6 paths
Fixes #6818

Change-Id: I71597a045c5b4117af69fba869cb616271c0dfe1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-21 08:34:49 -08:00
Brad Fitzpatrick
e36cdacf70 envknob: add time.Duration knob support
Updates #6818

Change-Id: I9c8147c02fb514f9f6f1f272bdb0f974c8b3ccbb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-21 08:34:49 -08:00
andig
14e8afe444 go.mod, etc: bump gvisor
Fixes #6554

Change-Id: Ia04ae37a47b67fa57091c9bfe1d45a1842589aa8
Signed-off-by: andig <cpuidle@gmx.de>
2022-12-20 22:02:40 -08:00
Brad Fitzpatrick
8aac77aa19 cmd/tailscale: fix "up" warning about netfilter-mode on Synology
Fixes #6811

Change-Id: Ia43723e6ebedc9b01729897cec271c462b16e9ae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-20 12:03:17 -08:00
Brad Fitzpatrick
f837d179b9 ssh/tailssh: fix typo in error message
"look up" is the verb. "lookup" is a noun.

Change-Id: I81c99e12c236488690758fb5c121e7e4e1622a36
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-20 11:42:26 -08:00
Brad Fitzpatrick
2eff9c8277 wgengine/magicsock: avoid ReadBatch/WriteBatch on old Linux kernels
Fixes #6807

Change-Id: I161424ef8a7338e1941d5e43d72dc6529993a0e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-20 10:54:24 -08:00
Andrew Dunham
0372e14d79 net/dns: bump DNS-over-TCP size limit to 4k
We saw a few cases where we hit this limit; bumping to 4k seems
relatively uncontroversial.

Change-Id: I218fee3bc0d2fa5fde16eddc36497a73ebd7cbda
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-12-20 12:08:19 -05:00
License Updater
98daf99775 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-19 20:56:17 -08:00
Brad Fitzpatrick
7c77c48bd4 go.toolchain.rev: bump Go
For:
dc0ce6324d
and
2cf198bc80

Updates #6792
Updates #6799

Change-Id: I58f022b5fb790e968938f90eb76e9dfdb74041fc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-19 20:30:34 -08:00
Brad Fitzpatrick
243490f932 go.mod: bump x/sys for linux/arm64 cpu SIGILL fix
Bump to get 2204b6615f

Updates #5793

Change-Id: I6ab78824047cb2c8d042f3f3bf47368ec6da5a34
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-19 19:18:33 -08:00
David Anderson
a1ded4c166 cmd/sync-containers: add a dry-run option.
Updates tailscale/corp#8461

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-19 19:00:53 -08:00
David Anderson
e5fe205c31 cmd/sync-containers: program to sync tags between container registries.
Updates tailscale/corp#8461

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-19 18:17:34 -08:00
License Updater
237f030cd9 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-19 14:05:59 -08:00
Aaron Klotz
296f53524c netstat, portlist: update Windows implementation to disambiguate svchost processes
We change our invocations of GetExtendedTcpTable to request additional
information about the "module" responsible for the port. In addition to pid,
this output also includes sufficient metadata to enable Windows to resolve
process names and disambiguate svchost processes.

We store the OS-specific output in an OSMetadata field in netstat.Entry, which
portlist may then use as necessary to actually resolve the process/module name.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-12-19 15:38:49 -06:00
Brad Fitzpatrick
a06217a8bd cmd/tailscale/cli: hide Windows named pipe default name in flag help
It's long & distracting for how low value it is.

Fixes #6766

Change-Id: I51364f25c0088d9e63deb9f692ba44031f12251b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-19 13:38:08 -08:00
Brad Fitzpatrick
5caf609d7b go.toolchain.rev: bump Go to 1.19.4
Updates tailscale/go#36

Change-Id: I0b741c18ef0286b511a79ec39b1e91464c7ce77b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-19 13:30:12 -08:00
Brad Fitzpatrick
0f604923d3 ipn/ipnlocal: fix StatusWithoutPeers not populating parts of Status
Fixes #4311

Change-Id: Iaae0615148fa7154f4ef8f66b455e3a6c2fa9df3
Co-authored-by: Claire Wang <claire@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-19 13:15:28 -08:00
Aaron Klotz
3c452b9880 util/winutil: fix erroneous condition in implementation of getRegIntegerInternal
We only want to log when err != registry.ErrNotExist. The condition was backward.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-12-19 15:12:53 -06:00
License Updater
14d07b7b20 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-19 11:55:27 -08:00
Jordan Whited
914d115f65 go.mod: bump tailscale/wireguard-go for big-endian fix (#6785)
Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-12-19 11:49:06 -08:00
David Anderson
af3127711a cmd/containerboot: allow disabling secret storage in k8s.
In some configurations, user explicitly do not want to store
tailscale state in k8s secrets, because doing that leads to
some annoying permission issues with sidecar containers.
With this change, TS_KUBE_SECRET="" and TS_STATE_DIR=/foo
will force storage to file when running in kubernetes.

Fixes #6704.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-16 15:46:38 -08:00
License Updater
6d5527e4b3 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-16 12:02:42 -08:00
Joe Tsai
d9df023e6f net/connstats: enforce maximum number of connections (#6760)
The Tailscale logging service has a hard limit on the maximum
log message size that can be accepted.
We want to ensure that netlog messages never exceed
this limit otherwise a client cannot transmit logs.

Move the goroutine for periodically dumping netlog messages
from wgengine/netlog to net/connstats.
This allows net/connstats to manage when it dumps messages,
either based on time or by size.

Updates tailscale/corp#8427

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-12-16 10:14:00 -08:00
Brad Fitzpatrick
651e0d8aad ssh/tailssh: add envknob for default PATH
As backup plan, just in case the earlier fix's logic wasn't correct
and we want to experiment in the field or have users have a quicker
fix.

Updates #5285

Change-Id: I7447466374d11f8f609de6dfbc4d9a944770826d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-15 15:01:04 -08:00
License Updater
fc0fe99edf licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-15 10:49:12 -08:00
Brad Fitzpatrick
c02ccf6424 go.mod: bump dhcp dep to remove another endian package from our tree
To pull in insomniacslk/dhcp#484 to pull in u-root/uio#8

Updates golang/go#57237

Change-Id: I1e56656e0dc9ec0b870f799fe3bc18b3caac1ee4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-15 10:47:25 -08:00
License Updater
97c615889d licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-15 10:10:19 -08:00
License Updater
a8f07153a6 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-15 09:11:33 -08:00
Anton Tolchanov
53c4892841 ipn/ipnserver: propagate http.Serve error
This ensures that we capture error returned by `Serve` and exit with a
non-zero exit code if it happens.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-12-15 15:01:56 +00:00
David Anderson
8171eb600c cmd/k8s-operator: move the operator into its own namespace.
The operator creates a fair bit of internal cluster state to manage proxying,
dumping it all in the default namespace is handy for development but rude
for production.

Updates #502

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-14 20:05:35 -08:00
Brad Fitzpatrick
56f7da0cfd ssh/tailssh: set default Tailscale SSH $PATH for non-interactive commands
Fixes #5285

Co-authored-by: Andrew Dunham <andrew@tailscale.com>
Change-Id: Ic7e967bf6a53b056cac5f21dd39565d9c31563af
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-14 18:45:35 -08:00
Joe Tsai
350aab05e5 util/multierr: optimize New for nil cases (#6750)
Consider the following pattern:

	err1 := foo()
	err2 := bar()
	err3 := baz()
	return multierr.New(err1, err2, err3)

If err1, err2, and err3 are all nil, then multierr.New should not allocate.
Thus, modify the logic of New to count the number of distinct error values
and allocate the exactly needed slice. This also speeds up non-empty error
situation since repeatedly growing with append is slow.

Performance:

	name         old time/op    new time/op    delta
	Empty-24       41.8ns ± 2%     6.4ns ± 1%   -84.73%  (p=0.000 n=10+10)
	NonEmpty-24     120ns ± 3%      69ns ± 1%   -42.01%  (p=0.000 n=9+10)

	name         old alloc/op   new alloc/op   delta
	Empty-24        64.0B ± 0%      0.0B       -100.00%  (p=0.000 n=10+10)
	NonEmpty-24      168B ± 0%       88B ± 0%   -47.62%  (p=0.000 n=10+10)

	name         old allocs/op  new allocs/op  delta
	Empty-24         1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
	NonEmpty-24      3.00 ± 0%      2.00 ± 0%   -33.33%  (p=0.000 n=10+10)

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-12-14 18:17:38 -08:00
Jordan Whited
55b24009f7 net/tstun: don't return early from a partial tun.Read() (#6745)
Fixes #6730

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-12-14 16:29:34 -08:00
David Anderson
3a5fc233aa cmd/k8s-operator: use oauth credentials for API access.
This automates both the operator's initial login, and
provisioning/deprovisioning of proxies.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-14 14:05:28 -08:00
David Anderson
a7ab3429b6 cmd/k8s-operator: refactor reconcile loop, un-plumbing reconcile.Result.
We used to need to do timed requeues in a few places in the reconcile logic,
and the easiest way to do that was to plumb reconcile.Result return values
around. But now we're purely event-driven, so the only thing we care about
is whether or not an error occurred.

Incidentally also fix a very minor bug where headless services would get
completely ignored, rather than reconciled into the correct state. This
shouldn't matter in practice because you can't transition from a headful
to a headless service without a deletion, but for consistency let's avoid
having a path that takes no definite action if a service of interest does
exist.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-14 11:35:59 -08:00
Denton Gentry
da53b1347b cmd/gitops-pusher: support alternate api-server URLs
Fixes https://github.com/tailscale/coral/issues/90

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-12-14 05:07:12 -08:00
David Anderson
835a73cc1f cmd/k8s-operator: remove unnecessary timed requeue.
Previously, we had to do blind timed requeues while waiting for
the tailscale hostname, because we looked up the hostname through
the API. But now the proxy container image writes back its hostname
to the k8s secret, so we get an event-triggered reconcile automatically
when the time is right.

Updates #502

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 17:15:06 -08:00
David Anderson
d857fd00b3 cmd/k8s-operator: sprinkle debug logging throughout.
As is convention in the k8s world, use zap for structured logging. For
development, OPERATOR_LOGGING=dev switches to a more human-readable output
than JSON.

Updates #502

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 17:15:06 -08:00
David Anderson
8ccd707218 cmd/k8s-operator: remove times requeues in proxy deletion path.
Our reconcile loop gets triggered again when the StatefulSet object
finally disappears (in addition to when its deletion starts, as indicated
by DeletionTimestamp != 0). So, we don't need to queue additional
reconciliations to proceed with the remainder of the cleanup, that
happens organically.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 13:49:35 -08:00
David Anderson
c0fcab01ac client/tailscale: fix request object for key creation.
The request takes key capabilities as an argument, but wrapped in a parent
object.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 13:49:35 -08:00
Andrew Dunham
3f4d51c588 net/dns: don't send on closed channel when message too large
Previously, if a DNS-over-TCP message was received while there were
existing queries in-flight, and it was over the size limit, we'd close
the 'responses' channel. This would cause those in-flight queries to
send on the closed channel and panic.

Instead, don't close the channel at all and rely on s.ctx being
canceled, which will ensure that in-flight queries don't hang.

Fixes #6725

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8267728ac37ed7ae38ddd09ce2633a5824320097
2022-12-13 15:54:17 -05:00
Brad Fitzpatrick
44be59c15a wgengine/magicsock: fix panic in wireguard-go rate limiting path
Fixes #6686

Change-Id: I1055a87141b07261afed8e36c963a69f3be26088
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-13 10:51:55 -08:00
Andrew Dunham
0d47cd2284 wgengine/monitor: fix panic due to race on Windows
It's possible for the 'somethingChanged' callback to be registered and
then trigger before the ctx field is assigned; move the assignment
earlier so this can't happen.

Change-Id: Ia7ee8b937299014a083ab40adf31a8b3e0db4ec5
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-12-13 13:28:12 -05:00
David Anderson
d81a2b2ce2 Makefile: add a target for doing dev builds of the k8s operator.
Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 09:50:57 -08:00
David Anderson
9c77205ba1 cmd/k8s-operator: add more tests for "normal" paths.
Tests cover configuring a proxy through an annotation rather than a
LoadBalancerClass, and converting between those two modes at runtime.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 09:31:01 -08:00
David Anderson
c902190e67 cmd/k8s-operator: factor out some of the larger expected test outputs.
For other test cases, the operator is going to produce similar generated
objects in several codepaths, and those objects are large. Move them out
to helpers so that the main test code stays a bit more intelligible.

The top-level Service that we start and end with remains in the main test
body, because its shape at the start and end is one of the main things that
varies a lot between test cases.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-13 09:31:01 -08:00
David Anderson
8dbb3b8bbe cmd/k8s-operator: remove unused structs. Cleanup missed in #6718.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 21:31:06 -08:00
David Anderson
53a9cc76c7 cmd/k8s-operator: rename main.go -> operator.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 21:18:31 -08:00
David Anderson
bc8f5a7734 cmd/k8s-operator: add a basic unit test.
The test verifies one of the successful reconcile paths, where
a client requests an exposed service via a LoadBalancer class.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 21:18:31 -08:00
David Anderson
3b7ae39a06 cmd/k8s-operator: use the client's authkey method to create auth keys.
Also introduces an intermediary interface for the tailscale client, in
preparation for operator tests that fake out the Tailscale API interaction.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 21:18:31 -08:00
Brad Fitzpatrick
ca08e316af util/endian: delete package; use updated josharian/native instead
See josharian/native#3

Updates golang/go#57237

Change-Id: I238c04c6654e5b9e7d9cfb81a7bbc5e1043a84a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-12 20:12:45 -08:00
Joe Tsai
bd2995c14b ipn/ipnlocal: simplify redactErr (#6716)
Use multierr.Range to iterate through an error tree
instead of multiple invocations of errors.As.
This scales better as we add more Go error types to the switch.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-12-12 17:51:03 -08:00
Joe Tsai
c47578b528 util/multierr: add Range (#6643)
Errors in Go are no longer viewed as a linear chain, but a tree.
See golang/go#53435.

Add a Range function that iterates through an error
in a pre-order, depth-first order.
This matches the iteration order of errors.As in Go 1.20.

This adds the logic (but currently commented out) for having
Error implement the multi-error version of Unwrap in Go 1.20.
It is commented out currently since it causes "go vet"
to complain about having the "wrong" signature.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-12-12 16:48:11 -08:00
Dave Anderson
041a0e3c27 client/tailscale: add APIs for auth key management. (#6715)
client/tailscale: add APIs for key management.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 16:01:04 -08:00
David Anderson
b2d4abf25a cmd/k8s-operator: add a kubernetes operator.
This was initially developed in a separate repo, but for build/release
reasons and because go module management limits the damage of importing
k8s things now, moving it into this repo.

At time of commit, the operator enables exposing services over tailscale,
with the 'tailscale' loadBalancerClass. It also currently requires an
unreleased feature to access the Tailscale API, so is not usable yet.

Updates #502.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-12 13:20:02 -08:00
Mihai Parparita
47002d93a3 ipn/ipnlocal: add a few metrics for PeerAPI and LocalAPI
Mainly motivated by wanting to know how much Taildrop is used, but
also useful when tracking down how many invalid requests are
generated.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-12 10:07:18 -08:00
Aaron Klotz
53e2010b8a cmd/tailscaled: change Windows implementation to shut down subprocess via closing its stdin
We've been doing a hard kill of the subprocess, which is only safe as long as
both the cli and gui are not running and the subprocess has had the opportunity
to clean up DNS settings etc. If unattended mode is turned on, this is definitely
unsafe.

I changed babysitProc to close the subprocess's stdin to make it shut down, and
then I plumbed a cancel function into the stdin reader on the subprocess side.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-12-12 12:02:26 -06:00
缘生
9c67395334 feat(build): add support on Loongnix-Server (loong64) (#6233)
Makefile, .github/workflow: add tests, targets for GOARCH=loong64 (Loongnix)

Signed-off-by: ysicing <i@ysicing.me>
2022-12-11 20:16:40 -08:00
Brad Fitzpatrick
7b65b7f85c go.mod: bump tailscale/wireguard-go for loong64
Updates tailscale/tailscale#6233

Change-Id: I5ba1826e79be51c03b19f2b31d73024410be4970
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-10 22:24:43 -08:00
Brad Fitzpatrick
5a523fdc7f go.mod: update deps to add support for GOARCH=loong64
Updates tailscale/tailscale#6233

Change-Id: Ibbc8e42607342e4ab4fc0b365ed628d82b56864d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-10 15:01:39 -08:00
shayne
9d335aabb2 cmd/tailscale/cli: [ssh] fix typo in help text (#6694)
arugments => arguments

Signed-off-by: shayne <79330+shayne@users.noreply.github.com>
2022-12-10 00:07:27 -05:00
License Updater
ab4992e10d licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-09 16:11:21 -08:00
Jordan Whited
ea5ee6f87c all: update golang.zx2c4.com/wireguard to github.com/tailscale/wireguard-go (#6692)
This is temporary while we work to upstream performance work in
https://github.com/WireGuard/wireguard-go/pull/64. A replace directive
is less ideal as it breaks dependent code without duplication of the
directive.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-12-09 15:12:20 -08:00
Andrew Dunham
b63094431b wgengine/router: fix tests on systems with older Busybox 'ip' binary
Adjust the expected system output by removing the unsupported mask
component including and after the slash in expected output like:
  fwmask 0xabc/0xdef

This package's tests now pass in an Alpine container when the 'go' and
'iptables' packages are installed (and run as privileged so /dev/net/tun
exists).

Fixes #5928

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id1a3896282bfa36b64afaec7a47205e63ad88542
2022-12-09 13:37:35 -05:00
Maisem Ali
eb1adf629f net/tstun: reuse buffered packet from pool
We would call parsedPacketPool.Get() for all packets received in Read/Write.
This was wasteful and not necessary, fetch a single *packet.Parsed for
all packets.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-09 23:37:15 +05:00
Walter Poupore
383e203fd2 cmd/tailscale/cli: update lock status help strings (#6675)
Signed-off-by: Walter Poupore <walterp@tailscale.com>

Signed-off-by: Walter Poupore <walterp@tailscale.com>
2022-12-09 10:24:21 -08:00
Jordan Whited
76389d8baf net/tstun, wgengine/magicsock: enable vectorized I/O on Linux (#6663)
This commit updates the wireguard-go dependency and implements the
necessary changes to the tun.Device and conn.Bind implementations to
support passing vectors of packets in tailscaled. This significantly
improves throughput performance on Linux.

Updates #414

Signed-off-by: Jordan Whited <jordan@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
Co-authored-by: James Tucker <james@tailscale.com>
2022-12-08 17:58:14 -08:00
James Tucker
389238fe4a cmd/tailscale/cli: add workaround for improper named socket quoting in ssh command
This avoids the issue in the common case where the socket path is the
default path, avoiding the immediate need for a Windows shell quote
implementation.

Updates #6639

Signed-off-by: James Tucker <james@tailscale.com>
2022-12-08 16:43:06 -08:00
Mihai Parparita
bdc45b9066 wgengine/magicsock: fix panic when rebinding fails
We would replace the existing real implementation of nettype.PacketConn
with a blockForeverConn, but that violates the contract of atomic.Value
(where the type cannot change). Fix by switching to a pointer value
(atomic.Pointer[nettype.PacketConn]).

A longstanding issue, but became more prevalent when we started binding
connections to interfaces on macOS and iOS (#6566), which could lead to
the bind call failing if the interface was no longer available.

Fixes #6641

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-08 16:34:14 -08:00
Tom DNetto
e27f4f022e cmd/tailscale/cli: add progress to tailscale file cp
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-12-08 14:05:11 -08:00
Xe Iaso
d1a5757639 tsnet: add HTTP client method to tsnet.Server (#6669)
This allows tsnet services to make requests to other services in the
tailnet with the tsnet service identity instead of the identity of the
host machine. This also enables tsnet services to make requests to other
tailnet services without having to have the host machine join the
tailnet.

Signed-off-by: Xe Iaso <xe@tailscale.com>

Signed-off-by: Xe Iaso <xe@tailscale.com>
2022-12-08 16:08:17 -05:00
salman
2d271f3bd1 ipn/ipnlocal: disallow exit nodes from using exit nodes
Nodes which have both -advertise-exit-node and -exit-node in prefs
should continue have them until the next invocation of `tailscale up`.

Updates #3569.

Signed-off-by: salman <salman@tailscale.com>
2022-12-08 17:10:33 +03:00
License Updater
5f68763cb2 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-07 21:20:39 -08:00
shayne
98114bf608 cmd/tailscale/cli, ipn/localapi: add funnel status to status command (#6402)
Fixes #6400

open up GETs for localapi serve-config to allow read-only access to
ServeConfig

`tailscale status` will include "Funnel on" status when Funnel is
configured. Prints nothing if Funnel is not running.

Example:

 $ tailscale status
 <nodes redacted>

 # Funnel on:
 #     - https://node-name.corp.ts.net
 #     - https://node-name.corp.ts.net:8443
 #     - tcp://node-name.corp.ts.net:10000

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-12-07 22:17:40 -05:00
David Anderson
1b65630e83 cmd/containerboot: switch to IPN bus monitoring instead of polling.
We still have to shell out to `tailscale up` because the container image's
API includes "arbitrary flags to tailscale up", unfortunately. But this
should still speed up startup a little, and also enables k8s-bound containers
to update their device information as new netmap updates come in.

Fixes #6657

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-07 13:31:15 -08:00
Tom DNetto
55e0512a05 ipn/ipnlocal,cmd/tailscale: minor improvements to lock modify command
* Do not print the status at the end of a successful operation
 * Ensure the key of the current node is actually trusted to make these changes

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-12-07 10:28:21 -08:00
Aaron Klotz
98f21354c6 cmd/tailscaled: add a special command to tailscaled's Windows service for removing WinTun
WinTun is installed lazily by tailscaled while it is running as LocalSystem.
Based upon what we're seeing in bug reports and support requests, removing
WinTun as a lesser user may fail under certain Windows versions, even when that
user is an Administrator.

By adding a user-defined command code to tailscaled, we can ask the service to
do the removal on our behalf while it is still running as LocalSystem.

* The uninstall code is basically the same as it is in corp;
* The command code will be sent as a service control request and is protected by
  the SERVICE_USER_DEFINED_CONTROL access right, which requires Administrator.

I'll be adding follow-up patches in corp to engage this functionality.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-12-07 12:12:02 -06:00
David Anderson
367228ef82 cmd/containerboot: gracefully degrade if missing patch permissions in k8s.
Fixes #6629.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-07 09:11:32 -08:00
Andrew Dunham
a887ca7efe ipn/ipnlocal: improve redactErr to handle more cases
This handles the case where the inner *os.PathError is wrapped in
another error type, and additionally will redact errors of type
*os.LinkError. Finally, add tests to verify that redaction works.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie83424ff6c85cdb29fb48b641330c495107aab7c
2022-12-06 19:37:15 -05:00
David Anderson
e36c27bcd1 cmd/containerboot: check that k8s secret permissions are correct.
Updates #6629.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-06 15:54:04 -08:00
David Anderson
e79a1eb24a cmd/containerboot: refactor tests to have more explicit phases.
In preparation for making startup more complex with IPN bus watches.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-06 15:32:04 -08:00
David Anderson
e04aaa7575 cmd/containerboot: split tailscaled bringup and auth phases.
In preparation for reworking auth to use IPN bus watch.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-06 15:32:04 -08:00
David Anderson
a469ec8ff6 cmd/containerboot: fix some lint.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-12-06 15:32:04 -08:00
License Updater
c084c7d7ed licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-06 10:31:53 -08:00
Anton Tolchanov
5ff946a9e6 cmd/containerboot: fix TS_STATE_DIR environment variable
It's supposed to set `--statedir` rather than `--state` file.

Fixes #6634.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-12-06 08:44:58 -08:00
License Updater
7048024e04 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-06 04:54:26 -08:00
Brad Fitzpatrick
1598cd0361 net/tsaddr: remove ContainsFunc helpers (they're now in x/exp/slices)
x/exp/slices now has ContainsFunc (golang/go#53983) so we can delete
our versions.

Change-Id: I5157a403bfc1b30e243bf31c8b611da25e995078
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-05 18:50:24 -08:00
License Updater
4b34c88426 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-12-05 16:04:16 -08:00
Mihai Parparita
79f3a5d753 net/netns, net/interfaces: explicitly bind sockets to the default interface on all Darwin variants
We were previously only doing this for tailscaled-on-Darwin, but it also
appears to help on iOS. Otherwise, when we rebind magicsock UDP
connections after a cellular -> WiFi interface change they still keep
using cellular one.

To do this correctly when using exit nodes, we need to exclude the
Tailscale interface when getting the default route, otherwise packets
cannot leave the tunnel. There are native macOS/iOS APIs that we can
use to do this, so we allow those clients to override the implementation
of DefaultRouteInterfaceIndex.

Updates #6565, may also help with #5156

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-05 13:33:20 -08:00
Mihai Parparita
cb525a1aad cmd/tailscaled: fix typo in netstack variable name
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-05 11:53:48 -08:00
Julia Stein
3f16dec1bb api.md: change "admin panel" to "admin console"
Signed-off-by: Julia Stein <julia@tailscale.com>
2022-12-05 11:21:55 -08:00
Tom DNetto
9c773af04c ipn/ipnlocal: fix use of stale profile while processing netmap
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-12-05 11:06:32 -08:00
Denton Gentry
c933b8882c VERSION.txt: this is v1.35.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-12-05 09:51:51 -08:00
Brad Fitzpatrick
964d723aba ipn/{ipnserver,localapi}: fix InUseOtherUser handling with WatchIPNBus
Updates tailscale/corp#8222

Change-Id: I2d6fa6514c7b8d0f89fded35a2d44e7df27e6fb1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-03 18:58:19 -08:00
Maisem Ali
86b6ff61e6 ipn/ipnlocal: fix Prefs access without mu being held
Noticed while working on a different fix.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-03 11:44:39 +05:00
Maisem Ali
cdb924f87b ipn/ipnlocal: sanitize prefs in more notify code paths
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-03 11:44:39 +05:00
Denton Gentry
7c4d017636 paths: set QNAP socket to /tmp.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-12-02 22:07:42 -08:00
Brad Fitzpatrick
57124e2126 ipn/localapi: add debug (root-required) access to inject debug Notify
For testing of Windows GUI client.

Updates #6480

Change-Id: I42f7526d95723e14bed7085fb759e371b43aa9da
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-02 20:26:08 -08:00
Maisem Ali
96cad35870 cmd/tailscaled: rename variables to be more descriptive
renamed from `useNetstack` to `onlyNetstack` which is 1 letter more but
more descriptive because we always have netstack enabled and `useNetstack`
doesn't convey what it is supposed to be used for. e.g. we always use
netstack for Tailscale SSH.

Also renamed shouldWrapNetstack to handleSubnetsInNetstack as it was only used
to configure subnet routing via netstack.

Updates tailscale/corp#8020

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-03 03:11:13 +05:00
Mihai Parparita
a87e0b4ea8 ipn: update comments that refer to Options.Prefs
Things are slightly less tangled now that we've migrated prefs to the
backend (and renamed the field to LegacyMigrationPrefs).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-02 11:55:54 -08:00
Brad Fitzpatrick
b9dd3fa534 paths, version/distro: detect Synology DSM version better, use for socket path
Resolves a TODO in the code noted while discussing QNAP defaults.

Tested on DSM6 and DSM7.

Change-Id: Icce03ff41fafd7b3a358cfee16f2ed13d5cc3c5d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-02 09:54:08 -08:00
Mihai Parparita
5b8323509f cmd/tailscale/cli: use "account" instead of "profile" in user-visible text
Matches the UI clients

Updates #713

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-12-02 09:01:44 -08:00
Anton Tolchanov
d6dbefaa91 api.md: lowercase ACL field names
Making this match https://tailscale.com/kb/1018/acls

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-12-02 13:17:59 +00:00
Anton Tolchanov
71c0e8d428 api.md: update documentation for the "set ACL" endpoint
This documents the `If-Match: ts-default` header that can be set to only
overwrite the default ACL contents, and also briefly mentions a few of
the new top-level ACL fields.

Updates tailscale/terraform-provider-tailscale#182

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-12-02 13:17:59 +00:00
Brad Fitzpatrick
e1d7d072a3 ipn/ipnlocal: set Notify.Version on initial Notifies
Missed when added recently in 8dd1418774

Change-Id: Id682640a54c3717afe084edaf69258ad23031b6c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-12-01 19:36:57 -08:00
Maya Kaczorowski
d5b4d2e276 cmd/tailscale/cli: improve tailnet lock help (#6583)
Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>

Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2022-12-01 09:19:28 -08:00
Walter Poupore
74b47eaad6 cmd/tailscale/cli: Fix 'tailscale switch' error message (#6585)
Updates #713.

Signed-off-by: Walter Poupore <walterp@tailscale.com>

Signed-off-by: Walter Poupore <walterp@tailscale.com>
2022-12-01 08:17:16 -08:00
Maisem Ali
99aa335923 net/dns: [linux] log and add metric for dnsMode
I couldn't find any logs that indicated which mode it was running in so adding that.
Also added a gauge metric for dnsMode.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-01 19:57:08 +05:00
Maisem Ali
a5a3188b7e cmd/tailscale/cli: unhide login and switch subcommands
Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-12-01 18:38:04 +05:00
Maya Kaczorowski
a1084047ce cmd/tailscale/cli: capitalize Get (#6586)
Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>

Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2022-11-30 21:32:14 -08:00
Tom DNetto
74c1f632f6 types/key,cmd/tailscale/cli: support tlpub prefix for tailnet-lock keys
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-30 18:04:47 -08:00
Brad Fitzpatrick
8dd1418774 ipn{,/ipnlocal}: add ipn.NotifyInitial* flags to WatchIPNBus
To simplify clients getting the initial state when they subscribe.

Change-Id: I2490a5ab2411253717c74265a46a98012b80db82
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 17:50:51 -08:00
Brad Fitzpatrick
197a4f1ae8 types/ptr: move all the ptrTo funcs to one new package's ptr.To
Change-Id: Ia0b820ffe7aa72897515f19bd415204b6fe743c7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 17:50:51 -08:00
Brad Fitzpatrick
a277eb4dcf ipn/ipnlocal: add missing context cancel
If user's fn returned false and never canceled their ctx, we never
stopped the NotifyWatchEngineUpdates goroutine.

This was introduced recently (this cycle).

Change-Id: I3453966ac71e00727296ddd237ef845782f4e52e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 17:50:51 -08:00
Mihai Parparita
978d6af91a ipn/ipnlocal: tweak error handling in interfaces PeerAPI debug endpoint
We were writing the error when getting the default interface before
setting the content type, so we'd get HTML treated as plain text.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-30 17:47:37 -08:00
Brad Fitzpatrick
5bdca747b7 ipn/ipnlocal: fix netstack peerapi crash over IPv6
The peerapi IPv6 listener has a nil listener.
But we didn't need the listener's address anyway, so don't
try to use it.

Change-Id: I8e8a1a895046d129a3683973e732d9bed82f3b02
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 16:28:53 -08:00
Tom DNetto
f1ab11e961 ipn/ipnlocal,tailcfg: introduce capability to gate TKA init paths
Previously, `TAILSCALE_USE_WIP_CODE` was needed to hit a bunch of the TKA paths. With
this change:

 - Enablement codepaths (NetworkLockInit) and initialization codepaths (tkaBootstrapFromGenesisLocked via tkaSyncIfNeeded)
   require either the WIP envknob or CapabilityTailnetLockAlpha.
 - Normal operation codepaths (tkaSyncIfNeeded, tkaFilterNetmapLocked) require TKA to be initialized, or either-or the
   envknob / capability.
 - Auxillary commands (ie: changing tka keys) require TKA to be initialized.

The end result is that it shouldn't be possible to initialize TKA (or subsequently use any of its features) without being
sent the capability or setting the envknob on tailscaled yourself.

I've also pulled out a bunch of unnecessary checks for CanSupportNetworkLock().

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-30 13:50:22 -08:00
Tom DNetto
9a80b8fb10 cmd/tailscale,ipn: surface TKA-filtered peers in lock status command
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-30 13:25:31 -08:00
Tom DNetto
731be07777 cmd/tailscale/cli: show rotation key when suggesting lock sign command
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-30 13:25:21 -08:00
Andrew Dunham
a6dff4fb74 docs/webhooks: use subtle.ConstantTimeCompare for comparing signatures
Fixes #6572

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I58610c46e0ea1d3a878f91d154db3da4de9cae00
2022-11-30 11:58:25 -05:00
Brad Fitzpatrick
74744b0a4c ipn: be more consistent with omitempty on debug/rare Prefs
Change-Id: Ib0ec72f243cdb2aca8dd392644d4eed33c3f92e6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 04:33:27 -08:00
Brad Fitzpatrick
f710d1cb20 cmd/tailscale/cli: add set --unattended on Windows
Fixes #6567

Change-Id: I8cb57196c601466401f8602eb50456e7cf7c31ef
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-30 04:33:03 -08:00
License Updater
d2a51f03ce licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-29 16:24:40 -08:00
Denton Gentry
a200a23f97 Revert "cmd/tailscale: access QNAP via localhost"
When running `tailscale web` as a standalone process,
it was necessary to send auth requests to QTS using
localhost to avoid hitting the proxy recursively.

However running `tailscale web` as a process means it is
consuming RAM all the time even when it isn't actively
doing anything.

After switching back to the `tailscale web` CGI mode, we
don't need to specifically use localhost for QNAP auth.

This reverts commit e0cadc5496.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-29 16:17:34 -08:00
Maisem Ali
82ad585b5b ipn/ipnlocal: account for ControlURL when merging profiles
We merge/dedupe profiles based on UserID and NodeID, however we were not accounting for ControlURLs.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-30 04:39:06 +05:00
Maisem Ali
adc302f428 all: use named pipes on windows
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-30 04:05:26 +05:00
Tom DNetto
45042a76cd cmd/tailscale,ipn: store disallowed TKA's in prefs, lock local-disable
Take 2 of https://github.com/tailscale/tailscale/pull/6546

Builds on https://github.com/tailscale/tailscale/pull/6560

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-29 13:43:38 -08:00
Tom DNetto
c4980f33f7 ipn,types/persist: add DisallowedTKAStateIDs, refactor as view type
Supercedes https://github.com/tailscale/tailscale/pull/6557, precursor to trying https://github.com/tailscale/tailscale/pull/6546 again

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-29 12:29:42 -08:00
Brad Fitzpatrick
6d012547b6 ipn/ipnlocal: use double dash flag style
The Go style weirds people out so we try to stick to the more
well-known double hyphen style in docs.

Change-Id: Iad6db5c82cda37f6b7687eed7ecd9276f8fd94d6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-29 11:34:08 -08:00
Brad Fitzpatrick
390d1bb871 Revert "ipn,types/persist: store disallowed TKA's in prefs, lock local-disable"
This reverts commit f1130421f0.

It was submitted with failing tests (go generate checks)

Requires a lot of API changes to fix so rolling back instead of
forward.

Change-Id: I024e8885c0ed44675d3028a662f386dda811f2ad
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-29 11:20:26 -08:00
Tom DNetto
f1130421f0 ipn,types/persist: store disallowed TKA's in prefs, lock local-disable
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-29 10:31:02 -08:00
Aaron Klotz
659e7837c6 health, ipn/ipnlocal: when -no-logs-no-support is enabled, deny access to tailnets that have network logging enabled
We want users to have the freedom to start tailscaled with `-no-logs-no-support`,
but that is obviously in direct conflict with tailnets that have network logging
enabled.

When we detect that condition, we record the issue in health, notify the client,
set WantRunning=false, and bail.

We clear the item in health when a profile switch occurs, since it is a
per-tailnet condition that should not propagate across profiles.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-11-29 11:42:20 -06:00
Maisem Ali
ad41cbd9d5 ipn/ipnlocal: sanitize prefs before sending over IPN Bus
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-29 12:43:38 +05:00
Brad Fitzpatrick
0a10a5632b cmd/tailscaled: add TS_DEBUG_BACKEND_DELAY_SEC for testing async startup
This adds an envknob to make testing async startup more reproducible.
We want the Windows GUI to behave well when wintun is not (or it's
doing its initial slow driver installation), but during testing it's often
too fast to see that it's working. This lets it be slowed down.

Updates #6522

Change-Id: I6ae19f46e270ea679cbaea32a53888efcf2943a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 21:19:33 -08:00
Brad Fitzpatrick
256ba62e00 README.md: add commit message style bit
Change-Id: I0c9423b76f773e20f0216d826620e169f82ee37c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 20:45:18 -08:00
Brad Fitzpatrick
0cb2ccce7f safesocket: remove the IPN protocol support
Updates #6417

Change-Id: I78908633de842d83b2cc8b10a864a0f88ab1b113
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 20:44:59 -08:00
Brad Fitzpatrick
06c4c47d46 ipn: remove unused Backend interface
Only the macOS/iOS clients care about it still, so we'll move it
to their repo.

But keep a test that makes sure that LocalBackend continues to
implement it so we get an early warning sign before we break
macOS/iOS.

Change-Id: I56392b740fe55b4d28468b77124c821b5c46c22b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 19:03:27 -08:00
Joe Tsai
2e5d08ec4f net/connstats: invert network logging data flow (#6272)
Previously, tstun.Wrapper and magicsock.Conn managed their
own statistics data structure and relied on an external call to
Extract to extract (and reset) the statistics.
This makes it difficult to ensure a maximum size on the statistics
as the caller has no introspection into whether the number
of unique connections is getting too large.

Invert the control flow such that a *connstats.Statistics
is registered with tstun.Wrapper and magicsock.Conn.
Methods on non-nil *connstats.Statistics are called for every packet.
This allows the implementation of connstats.Statistics (in the future)
to better control when it needs to flush to ensure
bounds on maximum sizes.

The value registered into tstun.Wrapper and magicsock.Conn could
be an interface, but that has two performance detriments:

1. Method calls on interface values are more expensive since
they must go through a virtual method dispatch.

2. The implementation would need a sync.Mutex to protect the
statistics value instead of using an atomic.Pointer.

Given that methods on constats.Statistics are called for every packet,
we want reduce the CPU cost on this hot path.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-28 15:59:33 -08:00
Joe Tsai
35c10373b5 types/logid: move logtail ID types here (#6336)
Many packages reference the logtail ID types,
but unfortunately pull in the transitive dependencies of logtail.
Fix this problem by putting the log ID types in its own package
with minimal dependencies.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-28 15:25:47 -08:00
Anton Tolchanov
6cc6c70d70 derp: prevent concurrent access to multiForwarder map
Instead of iterating over the map to determine the preferred forwarder
on every packet (which could happen concurrently with map mutations),
store it separately in an atomic variable.

Fixes #6445

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-11-28 22:49:06 +00:00
Aaron Klotz
6e33d2da2b ipn/ipnauth, util/winutil: add temporary LookupPseudoUser workaround to address os/user.LookupId errors on Windows
I added util/winutil/LookupPseudoUser, which essentially consists of the bits
that I am in the process of adding to Go's standard library.

We check the provided SID for "S-1-5-x" where 17 <= x <= 20 (which are the
known pseudo-users) and then manually populate a os/user.User struct with
the correct information.

Fixes https://github.com/tailscale/tailscale/issues/869
Fixes https://github.com/tailscale/tailscale/issues/2894

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-11-28 15:53:34 -06:00
Brad Fitzpatrick
3b0de97e07 cmd/tailscaled: unify the two Windows paths + separate IPN server path
tailscaled on Windows had two entirely separate start-up paths for running
as a service vs in the foreground. It's been causing problems for ages.
This unifies the two paths, making them be the same as the path used
for every other platform.

Also, it uses the new async LocalBackend support in ipnserver.Server
so the Server can start serving HTTP immediately, even if tun takes
awhile to come up.

Updates #6535

Change-Id: Icc8c4f96d4887b54a024d7ac15ad11096b5a58cf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 13:31:49 -08:00
Brad Fitzpatrick
ea25ef8236 util/set: add new set package for SetHandle type
We use this pattern in a number of places (in this repo and elsewhere)
and I was about to add a fourth to this repo which was crossing the line.
Add this type instead so they're all the same.

Also, we have another Set type (SliceSet, which tracks its keys in
order) in another repo we can move to this package later.

Change-Id: Ibbdcdba5443fae9b6956f63990bdb9e9443cefa9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 10:44:17 -08:00
Tom DNetto
5c8d2fa695 cmd/tailscale,ipn: improve UX of lock init command, cosmetic changes
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-28 10:39:04 -08:00
Brad Fitzpatrick
e8cc78b1af ipn/ipnserver: change Server to let LocalBackend be supplied async
This is step 1 of de-special-casing of Windows and letting the
LocalAPI HTTP server start serving immediately, even while the rest of
the world (notably the Engine and its TUN device) are being created,
which can take a few to dozens of seconds on Windows.

With this change, the ipnserver.New function changes to not take an
Engine and to return immediately, not returning an error, and let its
Run run immediately. If its ServeHTTP is called when it doesn't yet
have a LocalBackend, it returns an error. A TODO in there shows where
a future handler will serve status before an engine is available.

Future changes will:

* delete a bunch of tailscaled_windows.go code and use this new API
* add the ipnserver.Server ServerHTTP handler to await the engine
  being available
* use that handler in the Windows GUI client

Updates #6522

Change-Id: Iae94e68c235e850b112a72ea24ad0e0959b568ee
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-28 09:48:33 -08:00
Brad Fitzpatrick
8049053f86 ipn/*: make new WindowsUserID type to consolidate docs
The "userID is empty everywhere but Windows" docs on lots of places
but not everywhere while using just a string type was getting
confusing. This makes a new type to wrap up those rules, however
weird/historical they might be.

Change-Id: I142e85a8e38760988d6c0c91d0efecedade81b9b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-27 12:08:07 -08:00
Brad Fitzpatrick
3b73727e39 cmd/tailscale: de-punycode hostnames in status display
Still show original, but show de-punycode version in parens,
similar to how we show DNS-less hostnames.

Change-Id: I7e57da5e4029c5b49e8cd3014c350eddd2b3c338
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-27 03:11:49 -08:00
Brad Fitzpatrick
5676d201d6 ipn: add a WatchIPNBus option bit to subscribe to EngineStatus changes
So GUI clients don't need to poll for it.

We still poll internally (for now!) but that's still cheaper. And will
get much cheaper later, without having to modify clients once they
start sending this bit.

Change-Id: I36647b701c8d1fe197677e5eb76f6894e8ff79f7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-26 15:51:33 -08:00
Brad Fitzpatrick
f45106d47c ipn/ipnserver: move Windows-specific code to tailscaled_windows.go
We'll eventually remove it entirely, but for now move get it out of ipnserver
where it's distracting and move it to its sole caller.

Updates #6522

Change-Id: I9c6f6a91bf9a8e3c5ea997952b7c08c81723d447
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-26 15:51:22 -08:00
Brad Fitzpatrick
109aa3b2fb cmd/tailscale: add start of "debug derp" subcommand
Updates #6526

Change-Id: I84e440a8bd837c383000ce0cec4ff36b24249e8b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-26 15:40:25 -08:00
Brad Fitzpatrick
b0545873e5 ipn/ipnserver: remove protoSwitchConn shenanigans; just use http.Server early
Now that everything's just HTTP, there's no longer a need to have a
header-sniffing net.Conn wraper that dispatches which route to
take. Refactor to just use an http.Server earlier instead.

Updates #6417

Change-Id: I12a2054db4e56f48660c46f81233db224fdc77cb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-26 11:02:54 -08:00
shayne
e567902aa9 gitignore: ignore direnv nix-shell environment cache (#6520)
Also, updated ignore desc for .vscode/.

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-26 09:30:00 -05:00
Brad Fitzpatrick
f3ba268a96 ipn/ipnserver: move BabysitProc to tailscaled_windows.go
It's only used by Windows. No need for it to be in ipn/ipnserver,
which we're trying to trim down.

Change-Id: Idf923ac8b6cdae8b5338ec26c16fb8b5ea548071
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-25 12:26:38 -08:00
Maisem Ali
699b39dec1 ipn/ipnlocal: drop LocalBackend.inServerMode
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 22:16:37 +05:00
Brad Fitzpatrick
7e016c1d90 ipn/ipnserver: remove IPN protocol server
Unused in this repo as of the earlier #6450 (300aba61a6)
and unused in the Windows GUI as of tailscale/corp#8065.

With this ipn.BackendServer is no longer used and could also be
removed from this repo. The macOS and iOS clients still temporarily
depend on it, but I can move it to that repo instead while and let its
migration proceed on its own schedule while we clean this repo up.

Updates #6417
Updates tailscale/corp#8051

Change-Id: Ie13f82af3eb9f96b3a21c56cdda51be31ddebdcf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-25 08:12:06 -08:00
License Updater
624d9c759b licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-25 20:58:08 +05:00
Denton Gentry
b8fe89d15f net/portmapper: add test for Huawei router
Updates https://github.com/tailscale/tailscale/issues/6320

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-25 07:42:31 -08:00
Brad Fitzpatrick
1fdfb0dd08 ipn/localapi: add "enginestatus" debug command to LocalAPI
To force an EngineStatus update to the IPN bus.

This is a temporary measure while migrating the Windows GUI entirely
to the LocalAPI and off the old IPN protocol. The old IPN protocol
had RequestEngineStatus and LocalAPI didn't.

Updates #6417

Change-Id: I8ff525fc3dd82bdd9d92c2bdad6db5b75609eacd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-25 07:28:09 -08:00
Maisem Ali
c258015165 ipn/ipnlocal,ipnserver: rename {,Set}CurrentUser to {,Set}CurrentUserID
Address comments from https://github.com/tailscale/tailscale/pull/6506#discussion_r1032454064

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 19:48:04 +05:00
Brad Fitzpatrick
0a842f353c ipn/ipnserver: move more connection acceptance logic to LocalBackend
Follow-up to #6467 and #6506.

LocalBackend knows the server-mode state, so move more auth checking
there, removing some bookkeeping from ipnserver.Server.

Updates #6417
Updates tailscale/corp#8051

Change-Id: Ic5d14a077bf0dccc92a3621bd2646bab2cc5b837
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-25 06:24:29 -08:00
Brad Fitzpatrick
5ea7c7d603 ipn/{ipnlocal,ipnserver}: add some comments
Change-Id: Ieb5917edaf572342b755caa458693512c7aece81
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-25 06:05:45 -08:00
Maisem Ali
732c3d2ed0 .github/workflows: use ubuntu-22.04 for qemu tests
Fixes #6507

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 18:43:24 +05:00
Maisem Ali
a3cd171773 ipn/ipnserver: remove Server.serverModeUser
We can just rely on LocalBackend.CurrentUser

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 18:43:16 +05:00
Maisem Ali
d321b0ea4f ipn/ipnlocal: add docs to LocalBackend.SetCurrentUserID
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 18:43:16 +05:00
Maisem Ali
992749c44c tsnet/example/tshello: use the correct LocalClient for certs
Fixes #6485

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-25 16:05:50 +05:00
shayne
0c4c66948b cmd/tailscale/cli: Improve messaging when Funnel is unavailable. (#6502)
There are three specific requirements for Funnel to work:
1) They must accept an invite.
2) They must enable HTTPS.
3) The "funnel" node attribute must be appropriately set up in the ACLs.

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-24 22:40:48 -05:00
License Updater
344abaf3d3 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-24 16:23:03 -08:00
Brad Fitzpatrick
250edeb3da ipn/ipnserver: only permit the pre-HTTP LocalAPI protocol on Windows
Updates #6417

Change-Id: I1c9dbee3f72969f703b3ff2dbbaa145a17db868b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-24 14:56:04 -08:00
Aaron Klotz
033bd94d4c cmd/tailscaled, wgengine/router: use wingoes/com for COM initialization instead of go-ole
This patch removes the crappy, half-backed COM initialization used by `go-ole`
and replaces that with the `StartRuntime` function from `wingoes`, a library I
have started which, among other things, initializes COM properly.

In particular, we should always be initializing COM to use the multithreaded
apartment. Every single OS thread in the process becomes implicitly initialized
as part of the MTA, so we do not need to concern ourselves as to whether or not
any particular OS thread has initialized COM. Furthermore, we no longer need to
lock the OS thread when calling methods on COM interfaces.

Single-threaded apartments are designed solely for working with Win32 threads
that have a message pump; any other use of the STA is invalid.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-11-24 14:52:23 -06:00
Charlotte Brandhorst-Satzkorn
d6021ae71c words: hybrid theory (#6404)
The animal, not the car kind.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-24 10:28:11 -08:00
Brad Fitzpatrick
b68d008fee envknob: add CanTaildrop (TS_DISABLE_TAILDROP) to disable taildrop on device
This matches CanSSHD (TS_DISABLE_SSH_SERVER) for administratively
disabling the code on a node, regardless of local or server configs.

This can be configured in /etc/default/tailscaled on Linux,
%ProgramData%\Tailscale\tailscaled-env.txt on Windows,
or /etc/tailscale/tailscaled-env.txt on Synology. (see getPlatformEnvFile)

Also delete some dead code and tidy up some docs.

Change-Id: I79a87c03e33209619466ea8aeb0f6651afcb8789
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-24 07:51:17 -08:00
Brad Fitzpatrick
20b27df4d0 tailcfg, ipn, controlclient: add MapResponse.ClientVersion, plumb to IPN bus
Updates #6480

Change-Id: I6321071425cd091148d8140d1eb24dd536bb7984
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-23 20:24:12 -08:00
Brad Fitzpatrick
4d3713f631 envknob: add GOOS func
Centralize the fake GOOS stuff, start to use it more. To be used more
in the future.

Change-Id: Iabacfbeaf5fca0b53bf4d5dbcdc0367f05a205f9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-23 18:35:43 -08:00
Brad Fitzpatrick
6e6f27dd21 ipn/ipnlocal: lock down unsigned peers more
Apparently there's no tracking bug?

Updates tailscale/corp#7515 for ingress/funnel at least.

Change-Id: I03bc54fdc1f53f9832ab8b51475b2d676c38d897
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-23 18:20:39 -08:00
James Tucker
dc75b7cfd1 cmd/derpprobe: add -once flag for one-off CLI diagnostics
Updates #6478

Signed-off-by: James Tucker <james@tailscale.com>
2022-11-23 17:41:12 -08:00
James Tucker
b1441d0044 cmd/stunc: add command for making debug stun requests
Signed-off-by: James Tucker <james@tailscale.com>
2022-11-23 15:38:54 -08:00
Brad Fitzpatrick
7bff7345cc ipn/ipnauth: start splitting ipnserver into new ipnauth package
We're trying to gut 90% of the ipnserver package. A lot will get
deleted, some will move to LocalBackend, and a lot is being moved into
this new ipn/ipnauth package which will be leaf-y and testable.

This is a baby step towards moving some stuff to ipnauth.

Update #6417
Updates tailscale/corp#8051

Change-Id: I28bc2126764f46597d92a2d72565009dc6927ee0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-23 10:57:02 -08:00
Brad Fitzpatrick
5f6fec0eba cmd/tailscale: fix 'debug local-creds' hostname
Fixes #6446

Change-Id: I82f0a3dcf6aca25b7f67265533ee30a9d939d86f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-23 09:58:32 -08:00
Andrew Dunham
ec790e58df net/dns: retry overwriting hosts file on Windows
Updates #5753

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I60f81bd3325d5ba5383b947c7a7aaa5b14e460f6
2022-11-23 11:36:50 -05:00
Maisem Ali
3a5d02cb31 ipn/ipnlocal: update comment about using FallbackResolvers for exit nodes
While reading the DNS code noticed that we were still using FallbackResolvers
in this code path but the comment was out of date.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-23 16:52:22 +05:00
Brad Fitzpatrick
300aba61a6 ipn, cmd/tailscale/cli: add LocalAPI IPN bus watch, Start, convert CLI
Updates #6417
Updates tailscale/corp#8051

Change-Id: I1ca360730c45ffaa0261d8422877304277fc5625
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-22 13:11:44 -08:00
Maisem Ali
d4f6efa1df ipn/ipnlocal: handle case when selected profile is deleted
Profile keys are not deleted but are instead set to `nil` which results
in getting a nil error and we were not handling that correctly.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-23 00:42:18 +05:00
Tom DNetto
b45b948776 ipn/ipnlocal: call initTKALocked on backend start
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-22 11:06:52 -08:00
License Updater
1ef4be2f86 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-22 10:43:22 -08:00
Tom DNetto
aeb80bf8cb ipn/ipnlocal,tka: generate a nonce for each TKA
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-22 10:31:22 -08:00
Tom DNetto
6708f9a93f cmd/tailscale,ipn: implement lock log command
This commit implements `tailscale lock log [--limit N]`, which displays an ordered list
of changes to network-lock state in a manner familiar to `git log`.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-22 10:30:59 -08:00
Tom DNetto
ed1fae6c73 ipn/ipnlocal: always tx TKA sync after enablement
By always firing off a sync after enablement, the control plane should know the node's TKA head
at all times.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-22 10:25:13 -08:00
Brad Fitzpatrick
0f7da5c7dc ipn{,/ipnlocal}, client/tailscale: move Taildrop recv notifications to LocalAPI HTTP method
Updates #6417

Change-Id: Iec544c477a0e5e9f1c6bf23555afec06255e2e22
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-22 08:49:13 -08:00
Anton Tolchanov
f053f16460 tsweb: export version metrics to Prometheus
This will allow tracking build versions and runtime versions in
Prometheus.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-11-22 15:50:10 +00:00
Maisem Ali
8d84178884 ipn/ipnlocal: stop storing serverURL in LocalBackend
It's only read in a couple of places and we can read from Prefs directly.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-22 16:51:36 +05:00
Maisem Ali
aeac4bc8e2 ipn/ipnlocal: account for currentUserID when iterating over knownProfiles
We were not checking the currentUserID in all code paths that looped over
knownProfiles. This only impacted multi-user Windows setups.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-22 15:31:17 +05:00
Maisem Ali
18c7c3981a ipn/ipnlocal: call checkPrefs in Start too
We were not calling checkPrefs on `opts.*Prefs` in (*LocalBackend).Start().

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-22 15:13:00 +05:00
Maisem Ali
41dd49391f tstest/integration: add --accept-risk=all to tailscale down
The test would fail if I was running it over SSH.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-21 17:33:37 -08:00
Brad Fitzpatrick
0480a925c1 ipn/ipnlocal: send Content-Security-Policy, etc to peerapi browser requests
Updates tailscale/corp#7948

Change-Id: Ie70e0d042478338a37b7789ac63225193e47a524
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-21 17:33:06 -08:00
Luke Rewega
b190c1667b words: add some cloven-hoofed ruminants (#6393)
The Bovini tribe is sorely underrepresented.

Signed-off-by: Luke Rewega <lrewega@c32.ca>
2022-11-21 16:49:00 -08:00
License Updater
5c9203669a licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-21 15:21:30 -08:00
Andrew Dunham
a0ef51f570 cmd/{tailscale,tailscaled}: embed manifest into Windows binaries
This uses a go:generate statement to create a bunch of .syso files that
contain a Windows resource file. We check these in since they're less
than 1KiB each, and are only included on Windows.

Fixes #6429

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0512c3c0b2ab9d8d8509cf2037b88b81affcb81f
2022-11-21 18:15:51 -05:00
Maisem Ali
b94b91c168 cmd/tailscale/cli: add ability to set short names for profiles
This adds a `--nickname` flag to `tailscale login|set`.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-22 04:03:24 +05:00
Maisem Ali
575fd5f22b ipn: add ability to name profiles
Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-22 04:03:24 +05:00
Mihai Parparita
33520920c3 all: use strs.CutPrefix and strs.CutSuffix more
Updates places where we use HasPrefix + TrimPrefix to use the combined
function.

Updates #5309

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-21 14:32:16 -08:00
Aaron Klotz
41e1d336cc net/dns: change windows DNS manager to use pointer receiver
This is safer given that we need to close the NRPT database.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-11-21 15:46:51 -06:00
shayne
bdd8ce6692 cmd/tailscale/cli: disallow empty text "" from serve CLI (#6416)
Current behavior is broken. tailscale serve text / "" returns no error
and shows up in tailscale serve status but requests return a 500
"empty handler".

Adds an error if the user passes in an empty string for the text
handler.

Closes #6405

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-20 15:04:58 -05:00
shayne
d1e1c025b0 tailcfg: add TailscaleFunnelEnabled to HostInfo (#6414)
Adding this convenience for tailscale/corp#8015 and to avoid leaking
WireIngress.

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-20 13:22:54 -05:00
Charlotte Brandhorst-Satzkorn
538f431d5d words: no explanation necessary (#6413)
Scales do not exist without a banana.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-20 00:01:01 -05:00
Charlotte Brandhorst-Satzkorn
aac3d5bdd1 words: good vibes only (#6412)
After consultation with Tom, it has been agreed that a vibe, or vibes,
can be felt in different quantifiable measures. That makes a vibe, or
vibes, a scale thus it must be immortalized.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-19 23:28:30 -05:00
Brad Fitzpatrick
039ea51ca6 ipn/ipnlocal: add health warning for unstable builds
Like the macOS About dialog.

Change-Id: Ic27f091e66e29d5eebe4e195eda97ed331d748fd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-19 14:22:39 -08:00
Brad Fitzpatrick
a26f23d949 ipn/ipnlocal: actually fill out PeerAPIURLs in PeerStatus
The earlier 5f6d63936f was not complete.

Updates tailscale/corp#7515

Change-Id: I35efca51d1584c48ef6834a7d29cd42d7c943628
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-19 13:35:39 -08:00
Charlotte Brandhorst-Satzkorn
063eeefdca words: where do penguins go when they lose their tail? (#6403)
To the retail store.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-19 12:37:48 -05:00
Charlotte Brandhorst-Satzkorn
92fa0313d0 words: this list is really sheeping up to be something (#6399)
Ewe wouldn't have thought it was started as a joke.


Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-19 07:17:00 -08:00
shayne
f52a6d1b8c cmd/tailscale/cli, ipn: move serve CLI funcs on to ServeConfig (#6401)
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-19 09:42:14 -05:00
Charlotte Brandhorst-Satzkorn
2847dd2aef words: you goatta be kid'in me (#6397)
My puns are so baaaaaaad

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-11-19 00:45:45 -05:00
mel
e2f8b84170 words: add "coyote" to the wordlist for tailnet names (#6396)
Signed-off-by: yotes <yotes@fastmail.com>
2022-11-19 00:22:46 -05:00
License Updater
2eb0687969 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-18 19:10:05 -08:00
Brad Fitzpatrick
3a168cc1ff wgengine/magicsock: ignore pre-disco (pre-0.100) peers
There aren't any in the wild, other than one we ran on purpose to keep
us honest, but we can bump that one forward to 0.100.

Change-Id: I129e70724b2d3f8edf3b496dc01eba3ac5a2a907
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-18 17:52:08 -08:00
Tom DNetto
2a991a3541 ipn/{localapi,ipnserver}: set a CSP for ServeHTMLStatus, refactor host check
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-18 16:13:09 -08:00
phirework
a011320370 magicsock: cleanup canp2p (#6391)
This renames canP2P in magicsock to canP2PLocked to reflect
expectation of mutex lock, fixes a race we discovered in the meantime,
and updates the current stats.

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-11-18 12:23:22 -08:00
Maisem Ali
f1ad26f694 ipn/ipnlocal: strip NetworkLockKey from Prefs
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-19 00:57:49 +05:00
Anton Tolchanov
f40bb199f5 tsweb: cache prometheus metric names & types
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-11-18 19:52:37 +00:00
Anton Tolchanov
3c27632ffe tsweb: avoid dashes in Prometheus metric names
Ideally we should strip other invalid characters too, but that would
call for a regexp replacement which increases the number of allocations
and makes `TestVarzHandlerSorting` fail.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-11-18 19:52:37 +00:00
Maisem Ali
dd50dcd067 ipn/ipnlocal: handle untagging nodes better
We would end up with duplicate profiles for the node as the UserID
would have chnaged. In order to correctly deduplicate profiles, we
need to look at both the UserID and the NodeID. A single machine can
only ever have 1 profile per NodeID and 1 profile per UserID.

Note: UserID of a Node can change when the node is tagged/untagged,
and the NodeID of a device can change when the node is deleted so we
need to check for both.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-18 14:00:19 +05:00
Brad Fitzpatrick
f18dde6ad1 ipn/ipnserver: validate Host header on debug ServeHTMLStatus status
Updates tailscale/corp#7948

Change-Id: I3a8c64f353af1eeae620812b2700ce4af4fbbc88
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-17 23:02:40 -08:00
Brad Fitzpatrick
a13753ae1e ipn/localapi: require POST to add a bugreport marker
The LocalClient.BugReport method already sends it via POST.

Updates tailscale/corp#7948

Change-Id: I98dbd558c99d4296d934baa5ebc97052c7413073
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-17 21:54:06 -08:00
Maisem Ali
b5299d7d0e portlist: wait for lsof cmd to exit
We were leaking processes otherwise.

Co-authored-by: Mihai Parparita <mihai@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-18 03:42:35 +05:00
shayne
a97369f097 cmd/tailscale/cli: flesh out serve CLI and tests (#6304)
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-17 16:09:43 -05:00
Maisem Ali
5f6d63936f ipn/ipnlocal: fill out PeerAPIURLs in PeerStatus
Updates tailscale/corp#7515

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-18 01:49:32 +05:00
Andrew Dunham
0af61f7c40 cmd/tailscale, util/quarantine: set quarantine flags on files from Taildrop
This sets the "com.apple.quarantine" flag on macOS, and the
"Zone.Identifier" alternate data stream on Windows.

Change-Id: If14f805467b0e2963067937d7f34e08ba1d1fa85
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-11-17 15:06:02 -05:00
Andrew Dunham
cec48743fb ipn/localapi: set security headers
Change-Id: I028b6ab91229e2f824e5a69856ca9e1844f7486e
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-11-17 10:59:13 -05:00
Will Norris
1b8c13e18a words: help mercat get her sparkle back
Signed-off-by: Will Norris <will@tailscale.com>
2022-11-16 23:27:13 -08:00
Maisem Ali
f3519f7b29 cmd/tailscale/cli: add login and switch subcommands
Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-17 10:27:56 +05:00
Joe Tsai
ec1e67b1ab tstime: fix ParseDuration for '6' digit (#6363)
The cutset provided to strings.TrimRight was missing the digit '6',
making it such that we couldn't parse something like "365d".

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-16 21:01:09 -08:00
Joe Tsai
eff62b7b1b logtail: remove MustParsePublicID (#6335)
This function is no longer necessary as you can trivially rewrite:

	logtail.MustParsePublicID(...)

with:

	must.Get(logtail.ParsePublicID(...))

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-16 15:38:27 -08:00
Maisem Ali
1de64e89cd ipn/ipnlocal: set Hostinfo.WireIngress when ingress enabled
Optimization for control.

Updates tailscale/corp#7515

Change-Id: Ie93b232ab3e543d53062b462bdc13e279176f7a9
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-17 02:39:25 +05:00
Mihai Parparita
b3da5de10f ipn/localapi: also allow localhost as the LocalAPI host
The Mac and iOS LocalAPI clients make requests to it.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-16 12:11:45 -08:00
Maisem Ali
b0736fe6f7 ipn/ipnlocal: move selfNode from peerAPIServer to peerAPIHandler
The peerAPIHandler is instantiated per PeerAPI call so it is
guaranteed to have the latest selfNode.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-17 01:02:03 +05:00
Maisem Ali
2f4fca65a1 ipn/ipnlocal: prevent duplicate profiles of the same user
Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-17 00:59:55 +05:00
Brad Fitzpatrick
e9c851b04b ipn/ipnlocal: also accept service IP IPv6 literal in brackets for quad100
The fix in 4fc8538e2 was sufficient for IPv6. Browsers (can?) send the
IPv6 literal, even without a port number, in brackets.

Updates tailscale/corp#7948

Change-Id: I0e429d3de4df8429152c12f251ab140b0c8f6b77
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-16 11:47:42 -08:00
Maisem Ali
296e712591 tailcfg: add CapabilityDebug
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-17 00:21:10 +05:00
Maisem Ali
1e78fc462c ipn/ipnlocal: add some validation to PeerAPI
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 23:33:53 +05:00
Maisem Ali
1f4669a380 all: standardize on LocalAPI
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 23:15:24 +05:00
Maisem Ali
22238d897b all: standardize on PeerAPI
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 22:49:13 +05:00
Brad Fitzpatrick
1b56acf513 ipn/ipnlocal: move LocalBackend.validPopBrowserURL empty check from caller
I was too late with review feedback to 513780f4f8.

Updates tailscale/corp#7948

Change-Id: I8fa3b4eba4efaff591a2d0bfe6ab4795638b7c3a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-16 09:43:43 -08:00
Maisem Ali
513780f4f8 ipn/ipnlocal: move URL validation to LocalBackend
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 21:54:58 +05:00
Andrew Dunham
4caca8619e ipn/localapi: serve files with application/octet-stream Content-Type
Updates tailscale/corp#7948

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I5f570c04974598c7abae4017e4a7a0f63492c87c
2022-11-16 11:36:15 -05:00
Brad Fitzpatrick
4fc8538e2f ipn/ipnlocal: check quad100 Host header in info page
Updates tailscale/corp#7948

Change-Id: I0ab61c764bff9ba8afaf9070db73e971eb018477
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-16 08:18:12 -08:00
Maisem Ali
49b0ce8180 ipn/ipnlocal: update profile on server sent profile changes
We were not updating the LoginProfile.UserProfile when a netmap
updated the UserProfile (e.g. when a node was tagged via the admin panel).

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 21:08:53 +05:00
Brad Fitzpatrick
976e88d430 client/tailscale/apitype: add LocalAPIHost const, use it
Removes duplication.

Updates tailcale/corp#7948

Change-Id: I564c912ecfde31ba2293124bb1316e433c2a10f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-16 08:07:51 -08:00
Brad Fitzpatrick
97319a1970 control/controlclient: filter PopBrowserURL values to https schemes
No need for http://, etc. In case a control server sends a bogus value
and GUIs don't also validate.

Updates tailscale/corp#7948

Change-Id: I0b7dd86aa396bdabd88f0c4fe51831fb2ec4175a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-16 07:54:52 -08:00
Maisem Ali
2d653230ef ssh/tailssh: only call CloseWrite when both stdout and stderr are done
Updates #5209

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-16 16:22:47 +05:00
Brad Fitzpatrick
6ea2d01626 ipn/ipnlocal: be more assertive about rules for applyPrefsToHostinfo (now Locked)
The old docs were too cagey.

Change-Id: I92c4fdc4165e7ca35c4537aebe51eb3604b56f6d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-15 21:32:01 -08:00
Mihai Parparita
d3878ecd62 ipn/ipnlocal: add client metrics for profile switching
Updates #713

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-15 21:30:39 -08:00
Brad Fitzpatrick
b08f37d069 tailcfg: add Hostinfo.WireIngress bool
Yet unused. Future optimization for control.

Updates tailscale/corp#7515

Change-Id: Icd755aa8b1d6ca61d16dfc124c28c9c56ebdfee5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-15 21:06:18 -08:00
Mihai Parparita
6d48a54b3d version: make IsSandboxedMacOS handle the IPNExtension binary too
It was previously only invoked from the CLI, which only runs from the
main .app. However, starting with #6022 we also invoke it from the
network extension.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-15 17:54:10 -08:00
Maisem Ali
235309adc4 all: store NL keys per profile
This moves the NetworkLock key from a dedicated StateKey to be part of the persist.Persist struct.
This struct is stored as part for ipn.Prefs and is also the place where we store the NodeKey.

It also moves the ChonkDir from "/tka" to "/tka-profile/<profile-id>". The rename was intentional
to be able to delete the "/tka" dir if it exists.

This means that we will have a unique key per profile, and a unique directory per profile.

Note: `tailscale logout` will delete the entire profile, including any keys. It currently does not
delete the ChonkDir.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-15 19:51:52 +00:00
License Updater
751f866f01 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-15 10:57:53 -08:00
Maisem Ali
fe81ee62d7 ipn/ipnlocal: do controlclient.Shutdown in a different goroutine
We do not need to wait for it to complete. And we might have to
call Shutdown from callback from the controlclient which might
already be holding a lock that Shutdown requires.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-15 20:45:38 +05:00
Denton Gentry
e0cadc5496 cmd/tailscale: access QNAP via localhost
QNAP 5.x works much better if we let Apache proxy
tailscale web, which means the URLs can no longer
be relative since apache sends us an internal
URL. Access QNAP authentication via
http://localhost:8080/ as documented in
https://download.qnap.com/dev/API_QNAP_QTS_Authentication.pdf

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-14 21:43:52 -08:00
Brad Fitzpatrick
1950e56478 tsnet: add Server.ControlURL option
As requested in #6250 from @majst01.

Change-Id: Ia4bc5c4ebc98cd67d07328a1a42b87574261ddde
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-14 12:57:48 -08:00
Brad Fitzpatrick
f81351fdef portlist: fix data race
Maisem spotted the bug. The initial getList call in NewPoller wasn't
making a clone (only the Run loop's getList calls).

Fixes #6314

Change-Id: I8ab8799fcccea8e799140340d0ff88a825bb6ff0
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-14 12:34:18 -08:00
Tom DNetto
42855d219b ipn/ipnlocal: fix checks for node-key presence in TKA logic
Found by tests in another repo. TKA code wasn't always checking enough to be sure a node-key was set for the current state.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-14 20:33:31 +00:00
Mihai Parparita
0cc65b4bbe ipn/localapi: add LocalAPI endpoints for profile switching
Exposes a REST-y API for interacting with the profile switching
introduced in #6022.

Updates #713

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-14 11:37:10 -08:00
Tom DNetto
3271daf7a3 cmd/tailscale,ipn: support disablement args in lock cli, implement disable
* Support specifiying disablement values in lock init command
 * Support specifying rotation key in lock sign command
 * Implement lock disable command
 * Implement disablement-kdf command

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-14 19:22:33 +00:00
Brad Fitzpatrick
fb392e34b5 net/tshttpproxy: don't ignore env-based HTTP proxies after system lookups fail
There was a mechanism in tshttpproxy to note that a Windows proxy
lookup failed and to stop hitting it so often. But that turns out to
fire a lot (no PAC file configured at all results in a proxy lookup),
so after the first proxy lookup, we were enabling the "omg something's
wrong, stop looking up proxies" bit for awhile, which was then also
preventing the normal Go environment-based proxy lookups from working.

This at least fixes environment-based proxies.

Plenty of other Windows-specific proxy work remains (using
WinHttpGetIEProxyConfigForCurrentUser instead of just PAC files,
ignoring certain types of errors, etc), but this should fix
the regression reported in #4811.

Updates #4811

Change-Id: I665e1891897d58e290163bda5ca51a22a017c5f9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-14 09:11:33 -08:00
Brad Fitzpatrick
96e1582298 cmd/tailscale/cli: add debug 2021 --verbose flag
Change-Id: Ic93c2a8451518755fb44b5c35272d9b9353fe4d4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-14 09:11:33 -08:00
Maisem Ali
a255a08ea6 tailcfg: bump capver for ingress
Updates tailscale/corp#7515

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-14 09:03:16 +05:00
Brad Fitzpatrick
13bee8e91c cmd/tailscale/cli: update serve debug set command after FUS profile change
The key changed, but also we have a localapi method to set it anyway, so
use that.

Updates tailscale/corp#7515

Change-Id: Ia08ea2509f0bdd9b59e4c5de53aacf9a7d7eda36
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-13 12:55:23 -08:00
Brad Fitzpatrick
7c285fe7ee ipn/ipnlocal: fix error message typo in ingress peerapi handler
Updates tailscale/corp#7515

Change-Id: I81edd7d9f8958eef61be71d3d34e545cd3347008
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-13 12:54:38 -08:00
Brad Fitzpatrick
3114eacbb8 ipn/ipnlocal: don't warn about serve listener failing on IPv6-less machines
Fixes #6303

Change-Id: Ie1ce12938f68dfa0533246bbe3b9d7f3e749a243
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-13 10:11:25 -08:00
Brad Fitzpatrick
90bd74fc05 net/dns: add a health warning when Linux /etc/resolv.conf is overwritten
Change-Id: I925b4d904bc7ed920bc5afee11e6dcb2ffc2fbfd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-13 08:00:27 -08:00
Brad Fitzpatrick
3f8e185003 health: add Warnable, move ownership of warnable items to callers
The health package was turning into a rando dumping ground. Make a new
Warnable type instead that callers can request an instance of, and
then Set it locally in their code without the health package being
aware of all the things that are warnable. (For plenty of things the
health package will want to know details of how Tailscale works so it
can better prioritize/suppress errors, but lots of the warnings are
pretty leaf-y and unrelated)

This just moves two of the health warnings. Can probably move more
later.

Change-Id: I51e50e46eb633f4e96ced503d3b18a1891de1452
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-13 08:00:27 -08:00
License Updater
b1a6d8e2b1 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-13 03:23:30 -08:00
Brad Fitzpatrick
001f482aca net/dns: make "direct" mode on Linux warn on resolv.conf fights
Run an inotify goroutine and watch if another program takes over
/etc/inotify.conf. Log if so.

For now this only logs. In the future I want to wire it up into the
health system to warn (visible in "tailscale status", etc) about the
situation, with a short URL to more info about how you should really
be using systemd-resolved if you want programs to not fight over your
DNS files on Linux.

Updates #4254 etc etc

Change-Id: I86ad9125717d266d0e3822d4d847d88da6a0daaa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-12 22:19:13 -08:00
Maisem Ali
b87cb2c4a5 ipn/ipnlocal: call restart backend on user changes
This makes it so that the backend also restarts when users change,
otherwise an extra call to Start was required.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-13 09:00:15 +05:00
Maisem Ali
8e85227059 cmd/tailscale/cli: [set] handle selectively modifying routes/exit node
Noticed this while debugging something else, we would reset all routes if
either `--advertise-exit-node` or `--advertise-routes` were set. This handles
correctly updating them.

Also added tests.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-13 08:17:51 +05:00
Maisem Ali
26d1fc867e ipn/ipnlocal: delete profile on logout
Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-13 07:32:24 +05:00
Brad Fitzpatrick
0544d6ed04 cmd/tailscale/cli: continue fleshing out serve CLI tests
The serve CLI doesn't exist yet, but we want nice tests for it when it
does exist.

Updates tailscale/corp#7515

Change-Id: Ib4c73d606242c4228f87410bbfd29bec52ca6c60
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-12 16:48:27 -08:00
Brad Fitzpatrick
b5ac9172fd cmd/tailscale/cli: move earlier shell test to its own files
(I should've done this to start with.)

Updates tailscale/corp#7515

Change-Id: I7fb88cf95772790fd415ecf28fc52bde95507641
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-12 14:07:27 -08:00
Maisem Ali
9e70daad6f ipn/ipnlocal: make TKA tests not have side effects
It left the envknob turned on which meant that running all the tests
in the package had different behavior than running just any one test.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-12 20:56:48 +05:00
Brad Fitzpatrick
29bc021dcd cmd/tailscale/cli: add outline of serve CLI tests
Updates tailscale/corp#7515

Change-Id: Ib3956d4756bdcea0da89238a3c520ce8ac8004ec
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-11 19:13:07 -08:00
shayne
74e892cbc2 ipn/ipnlocal: listen to serve ports on netmap addrs (#6282)
Updates tailscale/corp#7515

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-11 21:46:26 -05:00
Brad Fitzpatrick
cbc89830c4 tsnet: be stricter about arguments to Server.Listen
Fixes #6201

Change-Id: I14b2b8ce9bee838344a3fad4f305c78ab775f72e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-11 18:23:23 -08:00
Brad Fitzpatrick
08e110ebc5 cmd/tailscale: make "up", "status" warn if routes and --accept-routes off
Example output:

    # Health check:
    #     - Some peers are advertising routes but --accept-routes is false

Also, move "tailscale status" health checks to the bottom, where they
won't be lost in large netmaps.

Updates #2053
Updates #6266

Change-Id: I5ae76a0cd69a452ce70063875cd7d974bfeb8f1a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-11 10:56:50 -08:00
Brad Fitzpatrick
66b4a363bd net/dns/resolver: add yet another 4via6 DNS form that's hopefully more robust
$ dig +short @100.100.100.100 aaaa 10-2-5-3-via-7.foo-bar.ts.net
fd7a:115c:a1e0:b1a:0:7:a02:503

$ dig +short @100.100.100.100 aaaa 10-2-5-3-via-7
fd7a:115c:a1e0:b1a:0:7:a02:503

$ ping 10-2-5-3-via-7
PING 10-2-5-3-via-7(fd7a:115c:a1e0:b1a:0:7:a02:503 (fd7a:115c:a1e0:b1a:0:7:a02:503)) 56 data bytes
...

Change-Id: Ice8f954518a6a2fca8b2c04da7f31f61d78cdec4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-11 09:30:48 -08:00
License Updater
e0cd9e9dec licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-11 08:11:23 -08:00
Brad Fitzpatrick
6aab4fb696 cmd/tailscale/cli: start making cert output support pkcs12 (p12) output
If the --key-file output filename ends in ".pfx" or ".p12", use pkcs12
format.

This might not be working entirely correctly yet but might be enough for
others to help out or experiment.

Updates #2928
Updates #5011

Change-Id: I62eb0eeaa293b9fd5e27b97b9bc476c23dd27cf6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-11 07:55:09 -08:00
Maisem Ali
d585cbf02a wgengine/router: [bsd/darwin] remove and readd routes on profile change
Noticed when testing FUS on tailscale-on-macOS, that routing would break
completely when switching between profiles. However, it would start working
again when going back to the original profile tailscaled started with.

Turns out that if we change the addrs on the interface we need to remove and readd
all the routes.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-11 19:49:01 +05:00
Tom DNetto
4c31183781 cmd/tailscale,ipn: minor fixes to tailscale lock commands
* Fix broken add/remove key commands
 * Make lock status display whether the node is signed

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-11 07:48:40 -06:00
License Updater
c60e444696 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-11 04:20:29 -08:00
Maisem Ali
ae18cd02c1 ipn: add AdvertisesExitNode and AdminPageURL accessors to PrefsView
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-11 16:03:15 +05:00
Maisem Ali
6cc0036b40 ipn/ipnlocal: use updated prefs in tkaSyncIfNeeded
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-11 12:03:17 +05:00
Brad Fitzpatrick
329a0a8406 client/tailscale: remove some json.Unmarshal repetition, add helper
Change-Id: I73ece09895ad04c7d8c4a5673f9bd360be873b9f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 21:54:56 -08:00
Maisem Ali
f00a49667d control/controlclient: make Status.Persist a PersistView
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-11 10:45:47 +05:00
Maisem Ali
4d330bac14 ipn/ipnlocal: add support for multiple user profiles
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-11 10:45:47 +05:00
Brad Fitzpatrick
c9d6a9cb4d ipn/ipnlocal: add optional TLS termination on proxied TCP connections
Updates tailscale/corp#7515

Change-Id: Ib250fa20275971563adccfa72db48e0cec02b7a5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 21:35:16 -08:00
shayne
56dfdbe190 repo: add .vscode/ to .gitignore (#6278)
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-11 00:10:47 -05:00
Brad Fitzpatrick
f4a522fd67 client/tailscale: make a helper for json.Marshal'ed request bodies
Change-Id: I59eb1643addf8793856089690407fb45053c8e4d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 20:47:51 -08:00
License Updater
13cadeabcd licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-10 20:43:52 -08:00
Brad Fitzpatrick
69e4b8a359 client/tailscale: document ServeConfig accessors a bit more
Updates tailscale/corp#7515

Change-Id: Iecae581e4b34ce70b2df531bc95c6c390a398c38
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 20:38:07 -08:00
Pat Maddox
9bf3ef4167 ssh/tailssh: add Tailscale SSH (server) support on FreeBSD
Change-Id: I607194b6ef99205e777f3df93a74ffe1a2e0344c
Signed-off-by: Pat Maddox <pat@ratiopbc.com>
2022-11-10 20:25:23 -08:00
shayne
e3a66e4d2f ipn/localapi: introduce get/set config for serve (#6243)
Updates tailscale/corp#7515

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2022-11-10 22:58:40 -05:00
Brad Fitzpatrick
7b5866ac0a ipn/ipnlocal: support serving files/directories too
Updates tailscale/corp#7515

Change-Id: I7b4c924005274ba57763264313d70d2a0c55da30
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 18:09:35 -08:00
Denton Gentry
446057d613 scripts/installer.sh: add Nobara Linux.
Fixes https://github.com/tailscale/tailscale/issues/5763

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-10 14:25:26 -08:00
Mihai Parparita
7a07bc654b ipn/localapi: rename /profile to /pprof
Avoids name collision with profiles for user switching.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-10 12:02:16 -08:00
Joe Tsai
9a05cdd2b5 syncs: add Map (#6260)
Map is a concurrent safe map that is a trivial wrapper
over a Go map and a sync.RWMutex.

It is optimized for use-cases where the entries change often,
which is the opposite use-case of what sync.Map is optimized for.

The API is patterned off of sync.Map, but made generic.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-10 10:55:26 -08:00
Brad Fitzpatrick
d7bfef12cf ipn/ipnlocal: support https+insecure:// backend proxy targets
Updates tailscale/corp#7515

Change-Id: Ie50295c09e4a16959b37087d8165c4d7360db37f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 10:09:49 -08:00
Brad Fitzpatrick
9dfb0916c2 ipn/ipnlocal, tailcfg: wire up ingress peerapi
Updates tailscale/corp#7515

Co-authored-by: Shayne Sweeney <shayne@tailscale.com>
Change-Id: I7eac7b4ac37fd8e8a9e0469594c1e9e7dd0da666
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 09:48:10 -08:00
David Anderson
65f3dab4c6 cmd/containerboot: make a tests table, add more tests.
Also fix a bugs found while adding the tests, oops.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-10 09:14:27 -08:00
Aaron Klotz
73b8968404 ipn/ipnlocal: ensure Persist information is saved to server mode start state
Numerous issues have been filed concerning an inability to install and run
Tailscale headlessly in unattended mode, particularly after rebooting. The
server mode `Prefs` stored in `server-state.conf` were not being updated with
`Persist` state once the node had been succesfully logged in.

Users have been working around this by finagling with the GUI to make it force
a state rewrite. This patch makes that unnecessary by ensuring the required
server mode state is updated when prefs are updated by the control client.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-11-10 10:43:40 -06:00
Brad Fitzpatrick
32a4ff3e5f ipn/ipnlocal: implement the reverse proxy HTTP handler type
Updates tailscale/corp#7515

Change-Id: Icbfe57f44b9516388edc0556eb04a370a9e3e009
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 06:12:33 -08:00
Brad Fitzpatrick
6beb3184d5 ipn/ipnlocal: don't serve a TLS cert unless it has webserver config
Even if the name is right, or is configured on a different port.

Updates tailscale/corp#7515

Change-Id: I8b721968f3241af10d98431e1b5ba075223e6cd3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 06:12:33 -08:00
Brad Fitzpatrick
1a94c309ea ipn/ipnlocal: support web TLS ports other than 443
Updates tailscale/corp#7515

Change-Id: I87df50b1bc92efd1d8c538c2ad4f1222361e4d6b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 06:12:33 -08:00
Brad Fitzpatrick
4797bacb7c ipn/ipnlocal: send RST when serving an actionless TCPPortHandler
Updates tailscale/corp#7515

Change-Id: I790f1b5d1e8a887e39bb573b4610b8f37a3f5963
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-10 06:12:33 -08:00
David Anderson
2111357568 cmd/containerboot: add tests.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-09 19:15:55 -08:00
Brad Fitzpatrick
b683921b87 ipn/ipnlocal: add start of handling TCP proxying
Updates tailscale/corp#7515

Change-Id: I82d19b5864674b2169f25ec8e429f60a543e0c57
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-09 16:38:11 -08:00
Brad Fitzpatrick
4bccc02413 ipn/ipnlocal: use ServerConfig views internally
Updates tailscale/corp#7515

Change-Id: Ica2bc44b92d281d5ce16cee55b7ca51c7910145c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-09 16:38:11 -08:00
Joe Tsai
4de643b714 types/netlogtype: add constants for maximum serialized sizes of ConnectionCounts (#6163)
There is a finite limit to the maximum message size that logtail can upload.
We need to make sure network logging messages remain under this size.
These constants allow us to compute the maximum number of ConnectionCounts
we can buffer before we must flush.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-09 15:50:07 -08:00
Brad Fitzpatrick
25e26c16ee ipn/ipnlocal: start implementing web server bits of serve
Updates tailscale/corp#7515

Change-Id: I96f4016161ba3c370492da941274c6d9a234c2bb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-09 07:38:10 -08:00
Brad Fitzpatrick
c35dcd427f cmd/tailscale/cli: make dev-store-set debug command a bit more magic
Temporarily at least. Makes sharing scripts during development easier.

Updates tailscale/corp#7515

Change-Id: I0e7aa461accd2c60740c1b37f3492b6bb58f1be3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-09 06:47:42 -08:00
Brad Fitzpatrick
df5e40f731 ipn: add WebServerConfig, add views
cmd/viewer couldn't deal with that map-of-map. Add a wrapper type
instead, which also gives us a place to add future stuff.

Updates tailscale/corp#7515

Change-Id: I44a4ca1915300ea8678e5b0385056f0642ccb155
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-09 06:17:45 -08:00
Brad Fitzpatrick
79472a4a6e wgengine/netstack: optimize shouldProcessInbound, avoiding 4via6 lookups
All IPv6 packets for the self address were doing netip.Prefix.Contains
lookups.

If if we know they're for a self address (which we already previously
computed and have sitting in a bool), then they can't be for a 4via6
range.

Change-Id: Iaaaf1248cb3fecec229935a80548ead0eb4cb892
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-08 20:28:28 -08:00
Brad Fitzpatrick
2daf0f146c ipn/ipnlocal, wgengine/netstack: start handling ports for future serving
Updates tailscale/corp#7515

Change-Id: I966e936e72a2ee99be8d0f5f16872b48cc150258
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-08 19:39:07 -08:00
Andrew Dunham
acf5839dd2 wgengine/netstack: add tests for shouldProcessInbound
Inspired by #6235, let's explicitly test the behaviour of this function
to ensure that we're not processing things we don't expect to.

Change-Id: I158050a63be7410fb99452089ea607aaf89fe91a
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-11-08 15:59:57 -08:00
Andrew Dunham
e85613aa2d net/netcheck: don't use a space in the captive portal challenge
The derpers don't allow whitespace in the challenge.

Change-Id: I93a8b073b846b87854fba127b5c1d80db205f658
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-11-08 16:58:54 -05:00
ysicing
cba1312dab util/endian: add support on Loongnix-Server (loong64)
Change-Id: I8275d158779c749a4cae2b4457d390871b4b8e3c
Signed-off-by: ysicing <i@ysicing.me>
2022-11-08 08:24:15 -08:00
Brad Fitzpatrick
abfdcd0f70 wgengine/netstack: fix shouldProcessInbound peerapi non-SYN handling
It was eating TCP packets to peerapi ports to subnet routers.  Some of
the TCP flow's packets went onward, some got eaten.  So some TCP flows
to subnet routers, if they used an unfortunate TCP port number, got
broken.

Change-Id: Ifea036119ccfb081f4dfa18b892373416a5239f8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-08 08:07:56 -08:00
Brad Fitzpatrick
6d8320a6e9 ipn/{ipnlocal,localapi}: move most of cert.go to ipnlocal
Leave only the HTTP/auth bits in localapi.

Change-Id: I8e23fb417367f1e0e31483e2982c343ca74086ab
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-07 21:50:04 -08:00
Brad Fitzpatrick
9be8d15979 ipn/localapi: refactor some cert code in prep for a move
I want to move the guts (after the HTTP layer) of the certificate
fetching into the ipnlocal package, out of localapi.

As prep, refactor a bit:

* add a method to do the fetch-from-cert-or-as-needed-with-refresh,
  rather than doing it in the HTTP hander

* convert two methods to funcs, taking the one extra field (LocalBackend)
  then needed from their method receiver. One of the methods needed
  nothing from its receiver.

This will make a future change easier to reason about.

Change-Id: I2a7811e5d7246139927bb86e7db8009bf09b3be3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-07 21:07:22 -08:00
Mihai Parparita
847a8cf917 api.md: make it clearer where to get the tailnet name in API calls
We added the tailnet organization name to to the settings page with
tailscale/corp#6977, but the docs were not updated to reflect this.
We later also changed "tailnet name" to refer to the MagicDNS hostname
(tailscale/corp#7537), which further confuses things (see https://stackoverflow.com/questions/74132318).

Make it slightly clearer what is the expected value for tailnet names in
API calls and how to get it.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-07 16:33:19 -08:00
David Anderson
5e703bdb55 docs/k8s: add secrets patching permission to the tailscale role.
Fixes #6225.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-07 16:18:01 -08:00
David Anderson
6acc27a92f cmd/containerboot: be more targeted when enabling IP forwarding.
Only enable forwarding for an IP family if any forwarding is required
for that family.

Fixes #6221.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-07 15:55:34 -08:00
Brad Fitzpatrick
5bb7e0307c cmd/tailscale, ipn/ipnlocal: add debug command to write to StateStore for dev
Not for end users (unless directed by support). Mostly for ease of
development for some upcoming webserver work.

Change-Id: I43acfed217514567acb3312367b24d620e739f88
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-07 15:34:43 -08:00
David Anderson
bf2d3cd074 cmd/containerboot: don't write device ID when not in Kubernetes.
Fixes #6218.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-07 15:02:29 -08:00
David Anderson
e0669555dd cmd/containerboot: don't write device ID into non-existent secret.
Fixes #6211

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-07 12:51:58 -08:00
Xe Iaso
be7556aece tsnet/example/tshello: use strings.Cut (#6198)
strings.Cut allows us to be more precise here. This example was written
before strings.Cut existed.

Signed-off-by: Xe <xe@tailscale.com>

Signed-off-by: Xe <xe@tailscale.com>
2022-11-07 15:06:34 -05:00
Andrew Dunham
c2d7940ec0 cmd/tailscaled, net/tstun: add build tags to omit BIRD and TAP
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I7a39f4eeeb583b73ecffaf4c5f086a38e3a53e2e
2022-11-07 11:13:14 -05:00
Brad Fitzpatrick
036334e913 net/netcheck: deflake (maybe) magicsock's TestNewConn
Updates #6207

Change-Id: I51d200d0b42b9a1ef799d0abfc8d4bd871c50cf2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-05 22:02:13 -07:00
Brad Fitzpatrick
db2cc393af util/dirwalk, metrics, portlist: add new package for fast directory walking
This is similar to the golang.org/x/tools/internal/fastwalk I'd
previously written but not recursive and using mem.RO.

The metrics package already had some Linux-specific directory reading
code in it. Move that out to a new general package that can be reused
by portlist too, which helps its scanning of all /proc files:

    name                old time/op    new time/op    delta
    FindProcessNames-8    2.79ms ± 6%    2.45ms ± 7%  -12.11%  (p=0.000 n=10+10)

    name                old alloc/op   new alloc/op   delta
    FindProcessNames-8    62.9kB ± 0%    33.5kB ± 0%  -46.76%  (p=0.000 n=9+10)

    name                old allocs/op  new allocs/op  delta
    FindProcessNames-8     2.25k ± 0%     0.38k ± 0%  -82.98%  (p=0.000 n=9+10)

Change-Id: I75db393032c328f12d95c39f71c9742c375f207a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-05 16:26:51 -07:00
Brad Fitzpatrick
21ef7e5c35 portlist: add macOS osImpl, finish migration to new style
Previously:

* 036f70b7b4 for linux
* 35bee36549 for windows

This does macOS.

And removes all the compat code for the old style. (e.g. iOS, js are
no longer mentioned; all platforms without implementations just
default to not doing anything)

One possible regression is that platforms without explicit
implementations previously tried to do the "netstat -na" style to get
open ports (but not process names). Maybe that worked on FreeBSD and
OpenBSD previously, but nobody ever really tested it. And it was kinda
useless without associated process names. So better off removing those
for now until they get a good implementation.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-04 10:58:23 -07:00
Brad Fitzpatrick
da8def8e13 all: remove old +build tags
The //go:build syntax was introduced in Go 1.17:

https://go.dev/doc/go1.17#build-lines

gofmt has kept the +build and go:build lines in sync since
then, but enough time has passed. Time to remove them.

Done with:

    perl -i -npe 's,^// \+build.*\n,,' $(git grep -l -F '+build')

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-04 07:25:42 -07:00
Maisem Ali
bb2cba0cd1 ipn: add missing check for nil Notify.Prefs
This was missed in 6afe26575c

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-04 04:12:53 +05:00
Maisem Ali
6afe26575c ipn: make Notify.Prefs be a *ipn.PrefsView
It is currently a `ipn.PrefsView` which means when we do a JSON roundtrip,
we go from an invalid Prefs to a valid one.

This makes it a pointer, which fixes the JSON roundtrip.

This was introduced in 0957bc5af2.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-04 04:00:26 +05:00
Adrian Dewhurst
c3a5489e72 util/winutil: remove log spam for missing registry keys
It's normal for HKLM\SOFTWARE\Policies\Tailscale to not exist but that
currently produces a lot of log spam.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2022-11-03 18:55:39 -04:00
David Anderson
76904b82e7 cmd/containerboot: PID1 for running tailscaled in a container.
This implements the same functionality as the former run.sh, but in Go
and with a little better awareness of tailscaled's lifecycle.

Also adds TS_AUTH_ONCE, which fixes the unfortunate behavior run.sh had
where it would unconditionally try to reauth every time if you gave it
an authkey, rather than try to use it only if auth is actually needed.
This makes it a bit nicer to deploy these containers in automation, since
you don't have to run the container once, then go and edit its definition
to remove authkeys.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-11-03 15:30:32 -07:00
Maisem Ali
0759d78f12 tailcfg: bump CurrentCapabilityVersion for EarlyNoise
Updates #5972

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-04 02:44:56 +05:00
Maisem Ali
a413fa4f85 control/controlclient: export NoiseClient
This allows reusing the NoiseClient in other repos without having to reimplement the earlyPayload logic.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-04 01:44:43 +05:00
Brad Fitzpatrick
d57cba8655 net/tshttpproxy: add clientmetrics on Windows proxy lookup paths
To collect some data on how widespread this is and whether there's
any correlation between different versions of Windows, etc.

Updates #4811

Change-Id: I003041d0d7e61d2482acd8155c1a4ed413a2c5c4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-03 12:16:42 -07:00
Brad Fitzpatrick
e55ae53169 tailcfg: add Node.UnsignedPeerAPIOnly to let server mark node as peerapi-only
capver 48

Change-Id: I20b2fa81d61ef8cc8a84e5f2afeefb68832bd904
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-02 21:55:04 -07:00
Brad Fitzpatrick
3367136d9e wgengine/netstack: remove old unused handleSSH hook
It's leftover from an earlier Tailscale SSH wiring and I forgot to
delete it apparently.

Change-Id: I14f071f450e272b98d90080a71ce68ba459168d1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-02 20:44:31 -07:00
License Updater
18fa1a0ad7 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-02 14:52:33 -07:00
Joe Tsai
2327c6b05f wgengine/netlog: preserve Tailscale addresses for exit traffic (#6165)
Exit node traffic is aggregated to protect the privacy
of those using an exit node. However, it is reasonable to
at least log which nodes are making most use of an exit node.

For a node using an exit node,
the source will be the taiscale IP address of itself,
while the destination will be zeroed out.

For a node that serves as an exit node,
the source will be zeroed out,
while the destination will be tailscale IP address
of the node that initiated the exit traffic.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-02 14:25:31 -07:00
Andrew Dunham
e975cb6b05 ipn/ipnlocal: fix test flake when we log after a test completes
This switches from using an atomic.Bool to a mutex for reasons that are
described in the commit, and should address the flakes that we're still
seeing.

Fixes #3020

Change-Id: I4e39471c0eb95886db03020ea1ccf688c7564a11
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-11-02 16:17:59 -04:00
Tom DNetto
0af57fce4c cmd/tailscale,ipn: implement lock sign command
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-11-02 15:00:01 -05:00
Joe Tsai
7d6775b082 wgengine: respect --no-logs-no-support flag for network logging (#6172)
In the future this will cause a node to be unable to join the tailnet
if network logging is enabled.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-11-02 12:57:04 -07:00
Brad Fitzpatrick
910db02652 client/tailscale, tsnet, ipn/ipnlocal: prove nodekey ownership over noise
Fixes #5972

Change-Id: Ic33a93d3613ac5dbf172d6a8a459ca06a7f9e547
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-02 09:22:26 -07:00
License Updater
8c790207a0 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-11-02 09:15:24 -07:00
Brad Fitzpatrick
a0ed2c2eb5 go.mod: bump golang-x-crypto
Fixes #5533

Change-Id: I403de0e9ed5a9a63055242a86720d6f4e0899af7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-02 09:01:11 -07:00
Andrew Dunham
06b55ab50f prober: fix test flake
This was tested by running 10000 test iterations and observing no flakes
after this change was made.

Change-Id: Ib036fd03a3a17800132c53c838cc32bfe2961306
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-11-02 09:58:40 -04:00
Brad Fitzpatrick
988c1f0ac7 control/controlclient, tailcfg: add support for EarlyNoise payload before http/2
Not yet used, but skipped over, parsed, and tested.

Updates #5972

Change-Id: Icd00196959ce266ae16a6c9244bd5e458e2c2947
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-01 15:11:22 -07:00
Brad Fitzpatrick
a7f7e79245 cmd/tailscale/cli: hide old, useless --host-routes flag
It was from very early Tailscale and no longer makes sense.

Change-Id: I31b4e728789f26b0376ebe73aa1b4bbbb1d62607
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-01 10:54:49 -07:00
Brad Fitzpatrick
f4ff26f577 types/pad32: delete package
Use Go 1.19's new 64-bit alignment ~hidden feature instead.

Fixes #5356

Change-Id: Ifcbcb115875a7da01df3bc29e9e7feadce5bc956
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-11-01 09:03:54 -07:00
Aoang
60f77ba515 Fix vm ci tests clogging in fork repository pull request
fix: https://github.com/tailscale/tailscale/issues/5771
Signed-off-by: Aoang <aoang@x2oe.com>
2022-11-01 08:43:07 -07:00
Maisem Ali
1440742a1c ssh/tailssh: use root / as cmd.Dir when users HomeDir doesn't exist
Fixes #5224

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-11-01 16:33:44 +05:00
Mihai Parparita
2be951a582 cmd/tsconnect: fix null pointer dereference when DNS lookups fail
If we never had a chance to open the SSH session we should not try to
close it.

Fixes #6089

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-31 14:23:44 -07:00
License Updater
e2519813b1 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-31 12:11:10 -07:00
Maisem Ali
42f7ef631e wgengine/netstack: use 72h as the KeepAlive Idle time for Tailscale SSH
Setting TCP KeepAlives for Tailscale SSH connections results in them
unnecessarily disconnecting. However, we can't turn them off completely
as that would mean we start leaking sessions waiting for a peer to come
back which may have gone away forever (e.g. if the node was deleted from
the tailnet during a session).

Updates #5021

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-31 21:33:50 +05:00
Tom DNetto
d98305c537 cmd,ipn/ipnlocal,tailcfg: implement TKA disablement
* Plumb disablement values through some of the internals of TKA enablement.
 * Transmit the node's TKA hash at the end of sync so the control plane understands each node's head.
 * Implement /machine/tka/disable RPC to actuate disablement on the control plane.

There is a partner PR for the control server I'll send shortly.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-31 11:05:44 -05:00
Denton Gentry
3d8eda5b72 scripts/install.sh: add RHEL7.
Fixes https://github.com/tailscale/tailscale/issues/5729

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-31 05:35:32 -07:00
Denton Gentry
5677ed1e85 scripts/installer.sh: add Debian Sid (rolling release)
There is no VERSION_ID.

root@sid:~# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux bookworm/sid"
NAME="Debian GNU/Linux"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-31 05:35:32 -07:00
Denton Gentry
798dba14eb scripts/install.sh: add openSUSE Leap 15.4
Fixes https://github.com/tailscale/tailscale/issues/6095

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-31 05:35:32 -07:00
Brad Fitzpatrick
ea24895e08 client/tailscale/apitype, tailcfg: delete never used mysterious PerDomain field
It does nothing and never did and I don't think anybody remembers what
the original goal for it was.

Updates #5229 (fixes, but need to clean it up in another repo too)

Change-Id: I81cc6ff44d6d2888bc43e9145437f4c407907ea6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-30 20:46:36 -07:00
Brad Fitzpatrick
7ad636f5b7 cmd/tailscale/cli: flesh out "tailscale ssh" CLI docs
Per user feedback.

Fixes #5877

Change-Id: Ib70ad57ec2507244fc54745f4e43c0ce13f51e9c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-30 20:09:41 -07:00
Brad Fitzpatrick
3336d08d59 cmd/tailscale/cli: make set without args print usage
Make "tailscale set" by itself be equivalent to "tailscale set -h"
rather than just say "you did it wrong" and make people do another -h
step.

Change-Id: Iad2b2ddb2595c0121d2536de5b78648f3eded3e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-30 16:09:02 -07:00
Brad Fitzpatrick
7b6cd4e659 cmd/tailscale/cli: make set's usage match up's, other than defaults
Fixes #6082

Change-Id: I92dc4726d866dcfd87e93ff09772601851d92ccf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-30 15:59:51 -07:00
Brad Fitzpatrick
231b88cc51 control/controlclient: add start of noise+http2 upgrade test
Basic HTTP/2-over-noise client test. To be fleshed out in subsequent
commits that add more functionality to the noise client.

Updates #5972

Change-Id: I0178343523ef4ae8e8fc87bae53cbc81f4e32fde
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-30 05:17:59 -07:00
Anton Tolchanov
e25ab75795 net/dns: getting base DNS config is not supported on macOS
Instead of returning a custom error, use ErrGetBaseConfigNotSupported
that seems to be intended for this use case. This fixes DNS resolution
on macOS clients compiled from source.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-30 08:57:33 +00:00
Anton Tolchanov
193afe19cb ipn/ipnlocal: add tags and a few other details to self status
This makes tags, creation time, exit node option and primary routes
for the current node exposed via `tailscale status --json`

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-29 10:00:06 +01:00
Brad Fitzpatrick
120bfc97ce control/controlclient: refactor noiseClient, connections, http2
In prep for stateful http2 noise connections.

Updates #5972

Change-Id: I9ebecc3b2d5d193621b87d39b506f231d6c82145
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-28 15:49:36 -07:00
Mihai Parparita
4e6e3bd13d ipn/ipnlocal: fix a log line having function pointers instead of values
Followup to using ipn.PrefsView (#6031).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-28 15:46:41 -07:00
Joe Tsai
cfef47ddcc wgengine: perform router reconfig for netlog-only changes (#6118)
If the network logging configruation changes (and nothing else)
we will tear down the network logger and start it back up.
However, doing so will lose the router configuration state.
Manually reconfigure it with the routing state.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-28 15:33:24 -07:00
Brad Fitzpatrick
dfe67afb4a control/controlhttp: remove ClientConn.UntrustedUpgradeHeaders
It was just added and unreleased but we've decided to go a different route.

Details are in 5e9e57ecf5.

Updates #5972

Change-Id: I49016af469225f58535f63a9b0fbe5ab6a5bf304
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-28 12:25:22 -07:00
Joe Tsai
b2035a1dca cmd/netlogfmt: handle any stream of network logs (#6108)
Make netlogfmt useful regardless of the exact schema of the input.
If a JSON object looks like a network log message,
then unmarshal it as one and then print it.
This allows netlogfmt to support both a stream of JSON objects
directly serialized from netlogtype.Message, or the schema
returned by the /api/v2/tailnet/{{tailnet}}/network-logs API endpoint.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-28 10:40:45 -07:00
Joe Tsai
48ddb3af2a wgengine/netlog: enforce hard limit on network log message sizes (#6109)
This is a temporary hack to prevent logtail getting stuck
uploading the same excessive message over and over.
A better solution will be discussed and implemented.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-28 10:13:35 -07:00
Joe Tsai
a3602c28bd wgengine/netlog: embed the StableNodeID of the authoring node (#6105)
This allows network messages to be annotated with which node it came from.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-28 10:09:30 -07:00
Joe Tsai
81fd259133 wgengine/magicsock: gather physical-layer statistics (#5925)
There is utility in logging traffic statistics that occurs at the physical layer.
That is, in order to send packets virtually to a particular tailscale IP address,
what physical endpoints did we need to communicate with?

This functionality logs IP addresses identical to
what had always been logged in magicsock prior to #5823,
so there is no increase in PII being logged.

ExtractStatistics returns a mapping of connections to counts.
The source is always a Tailscale IP address (without port),
while the destination is some endpoint reachable on WAN or LAN.
As a special case, traffic routed through DERP will use 127.3.3.40
as the destination address with the port being the DERP region.

This entire feature is only enabled if data-plane audit logging
is enabled on the tailnet (by default it is disabled).

Example of type of information logged:

	------------------------------------  Tx[P/s]    Tx[B/s]  Rx[P/s]   Rx[B/s]
	PhysicalTraffic:                       25.80      3.39Ki   38.80     5.57Ki
	    100.1.2.3 -> 143.11.22.33:41641    15.40      2.00Ki   23.20     3.37Ki
	    100.4.5.6 -> 192.168.0.100:41641   10.20      1.38Ki   15.60     2.20Ki
	    100.7.8.9 -> 127.3.3.40:2           0.20      6.40      0.00     0.00

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-27 16:26:52 -07:00
Brad Fitzpatrick
5e9e57ecf5 control/controlhttp: add AcceptHTTP hook to add coalesced Server->Client write
New plan for #5972. Instead of sending the public key in the clear
(from earlier unreleased 246274b8e9) where the client might have to
worry about it being dropped or tampered with and retrying, we'll
instead send it post-Noise handshake but before the HTTP/2 connection
begins.

This replaces the earlier extraHeaders hook with a different sort of
hook that allows us to combine two writes on the wire in one packet.

Updates #5972

Change-Id: I42cdf7c1859b53ca4dfa5610bd1b840c6986e09c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-27 15:07:53 -07:00
Joe Tsai
c21a3c4733 types/netlogtype: new package for network logging types (#6092)
The netlog.Message type is useful to depend on from other packages,
but doing so would transitively cause gvisor and other large packages
to be linked in.

Avoid this problem by moving all network logging types to a single package.

We also update staticcheck to take in:

	003d277bcf

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-27 14:14:18 -07:00
Aaron Klotz
a44687e71f wgengine/winnet: invoke some COM methods directly instead of through IDispatch.
Intermittently in the wild we are seeing failures when calling
`INetworkConnection::GetNetwork`. It is unclear what the root cause is, but what
is clear is that the error is happening inside the object's `IDispatch` invoker
(as opposed to the method implementation itself).

This patch replaces our wrapper for `INetworkConnection::GetNetwork` with an
alternate implementation that directly invokes the method, instead of using
`IDispatch`. I also replaced the implementations of `INetwork::SetCategory` and
`INetwork::GetCategory` while I was there.

This patch is speculative and tightly-scoped so that we could possibly add it
to a dot-release if necessary.

Updates https://github.com/tailscale/tailscale/issues/4134
Updates https://github.com/tailscale/tailscale/issues/6037

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-10-27 14:05:31 -05:00
Brad Fitzpatrick
4021ae6b9d types/key: add missing ChallengePublic.UnmarshalText
Forgot it when adding the Challenge types earlier.

Change-Id: Ie0872c4e6dc25e5d832aa58c7b3f66d450bf6b71
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-27 11:12:38 -07:00
Adrian Dewhurst
8c09ae9032 tka, types/key: add NLPublic.KeyID
This allows direct use of NLPublic with tka.Authority.KeyTrusted() and
similar without using tricks like converting the return value of Verifier.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2022-10-26 15:51:23 -04:00
Sonia Appasamy
944f43f1c8 docs/webhooks: add sample endpoint code
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2022-10-26 14:28:00 -05:00
Andrew Dunham
95f3dd1346 net/interfaces: don't dereference null pointer if no destination/netmask
Fixes #6065

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I7159b8cbb8d5f47c0668cf83e59167f182f1defd
2022-10-26 10:28:26 -04:00
Maisem Ali
19b5586573 cmd/tailscale/cli: add beginnings of tailscale set
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-25 22:27:37 -07:00
Jordan Whited
a471681e28 wgengine/netstack: enable TCP SACK (#6066)
TCP selective acknowledgement can improve throughput by an order
of magnitude in the presence of loss.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-10-25 16:09:20 -07:00
Mihai Parparita
d60f7fe33f cmd/tsconnect: run wasm-opt on the generated wasm file
Saves about 1.4MB from the generated wasm file. The Brotli size is
basically unchanged (it's actually slightly larger, by 40K), suggesting
that most of the size delta is due to not inlining and other changes
that were easily compressible.

However, it still seems worthwhile to have a smaller final binary, to
reduce parse time and increase likelihood that we fit in the browser's
disk cache. Actual performance appears to be unchanged.

Updates #5142

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-25 13:16:37 -07:00
Maisem Ali
2a9ba28def ipn/ipnlocal: set prefs before calling tkaSyncIfNeeded
Caught this in a test in a different repo.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-25 11:57:42 -07:00
Will Norris
bff202a290 cmd/nginx-auth: add experimental status badge to README 2022-10-25 11:03:40 -07:00
Brad Fitzpatrick
35bee36549 portlist: use win32 calls instead of running netstat process [windows]
Turns out using win32 instead of shelling out to child processes is a
bit faster:

    name                  old time/op    new time/op    delta
    GetListIncremental-4     278ms ± 2%       0ms ± 7%  -99.93%  (p=0.000 n=8+10)

    name                  old alloc/op   new alloc/op   delta
    GetListIncremental-4     238kB ± 0%       9kB ± 0%  -96.12%  (p=0.000 n=10+8)

    name                  old allocs/op  new allocs/op  delta
    GetListIncremental-4     1.19k ± 0%     0.02k ± 0%  -98.49%  (p=0.000 n=10+10)

Fixes #3876 (sadly)

Change-Id: I1195ac5de21a8a8b3cdace5871d263e81aa27e91
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-25 10:55:25 -07:00
Andrew Dunham
527741d41f shell.nix: add graphviz
Change-Id: Ic25e11a056a7624ebba923d2b87d947e24c41a20
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-25 13:03:31 -04:00
License Updater
a1a2c165e9 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-25 09:49:50 -07:00
Denton Gentry
dafc822654 cmd/nginx-auth: increment version.
We need a new release to handle TCD changes
after MagicDNS GA

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-25 08:38:54 -07:00
Maisem Ali
9f39c3b10f ipn/ipnlocal: make EditPrefs strip private keys before returning
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-24 15:57:00 -07:00
Maisem Ali
a2d15924fb types/persist: add PublicNodeKey helper
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-24 15:57:00 -07:00
Maisem Ali
0957bc5af2 ipn/ipnlocal: use ipn.PrefsView
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-24 15:57:00 -07:00
Maisem Ali
20324eeebc ipn/prefs: add views
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-24 15:57:00 -07:00
Andrew Dunham
ba459aeef5 net/interfaces: don't call GetList in List.ForeachInterface
It looks like this was left by mistake in 4a3e2842.

Change-Id: Ie4e3d5842548cd2e8533b3552298fb1ce9ba761a
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-24 16:55:24 -04:00
Mihai Parparita
660abd7309 cmd/tsconnect: add README to generated NPM package
Makes the landing page at https://www.npmjs.com/package/@tailscale/connect
look slightly nicer.

Fixes #5976

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-24 13:51:32 -07:00
Denton Gentry
9beb07b4ff scripts/install.sh: add Ubuntu Kinetic Kudu
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-24 07:17:00 -07:00
Brad Fitzpatrick
575c599410 portlist: add a test that verifies changes are picked up over time
To avoid annoying firewall dialogs on macOS and Windows, only run it
on Linux by default without the flag.

Change-Id: If8486c31d4243ade54b0131f673237c6c9184c08
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-23 22:02:22 -07:00
Brad Fitzpatrick
036f70b7b4 portlist: refactor, introduce OS-specific types
Add an osImpl interface that can be stateful and thus more efficient
between calls. It will later be implemented by all OSes but for now
this change only adds a Linux implementation.

Remove Port.inode. It was only used by Linux and moves into its osImpl.

Don't reopen /proc/net/* files on each run. Turns out you can just
keep then open and seek to the beginning and reread and the contents
are fresh.

    name                    old time/op    new time/op    delta
    GetListIncremental-8    7.29ms ± 2%    6.53ms ± 1%  -10.50%  (p=0.000 n=9+9)

    name                   old alloc/op   new alloc/op   delta
    GetListIncremental-8    1.30kB ±13%    0.70kB ± 5%  -46.38%  (p=0.000 n=9+10)

    name                  old allocs/op  new allocs/op  delta
    GetListIncremental-8      33.2 ±11%      18.0 ± 0%  -45.82%  (p=0.000 n=9+10)

Updates #5958

Change-Id: I4be83463cbd23c2e2fa5d0bdf38560004f53401b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-23 20:29:23 -07:00
Peter Cai
4597ec1037 net/dnscache: Handle 4-in-6 addresses in DNS responses
On Android, the system resolver can return IPv4 addresses as IPv6-mapped
addresses (i.e. `::ffff:a.b.c.d`). After the switch to `net/netip`
(19008a3), this case is no longer handled and a response like this will
be seen as failure to resolve any IPv4 addresses.

Handle this case by simply calling `Unmap()` on the returned IPs. Fixes #5698.

Signed-off-by: Peter Cai <peter@typeblog.net>
2022-10-23 08:41:51 -07:00
Brad Fitzpatrick
70dde89c34 portlist: add package doc, file comments, move a method to the right file
And respect envknob earlier. NewPoller has one caller and ignores
errors; they just signal ipnlocal to log a warning and not use the
portlist poller.

Change-Id: I4a33af936fe780cca8c7197d4d74ac31a1dc01e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-22 21:44:40 -07:00
Brad Fitzpatrick
774fa72d32 portlist: add BenchmarkGetListIncremental
In contrast to BenchmarkGetList, this new BenchmarkGetListIncremental
acts like what happens in practice, remembering the previous run and
avoiding work that's already been done previously.

Currently:

    BenchmarkGetList
    BenchmarkGetList-8                           100          11011100 ns/op           68411 B/op       2211 allocs/op
    BenchmarkGetList-8                           100          11443410 ns/op           69073 B/op       2223 allocs/op
    BenchmarkGetList-8                           100          11217311 ns/op           68421 B/op       2197 allocs/op
    BenchmarkGetList-8                           100          11035559 ns/op           68801 B/op       2220 allocs/op
    BenchmarkGetList-8                           100          10921596 ns/op           69226 B/op       2225 allocs/op
    BenchmarkGetListIncremental
    BenchmarkGetListIncremental-8                168           7187217 ns/op            1192 B/op         28 allocs/op
    BenchmarkGetListIncremental-8                172           7004525 ns/op            1194 B/op         28 allocs/op
    BenchmarkGetListIncremental-8                162           7235889 ns/op            1221 B/op         29 allocs/op
    BenchmarkGetListIncremental-8                164           7035671 ns/op            1219 B/op         29 allocs/op
    BenchmarkGetListIncremental-8                174           7095448 ns/op            1114 B/op         27 allocs/op

Updates #5958

Change-Id: I1bd5a4b206df4173e2cb8e8a780429d9daa6ef1d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-22 20:52:28 -07:00
Pontus Leitzler
f36ddd9275 words: even the odds with something that reminds you of vacation (#6025)
The cute little salak belongs there. It also evens the odds if tails
start a mutiny against scales. Even though they outnumber scales, they
should still know their place. Behind.

Signed-off-by: Pontus Leitzler <leitzler@gmail.com>
2022-10-22 14:43:57 -07:00
Brad Fitzpatrick
3697609aaa portlist: remove unix.Readlink allocs on Linux
name       old time/op    new time/op    delta
    GetList-8    11.2ms ± 5%    11.1ms ± 3%     ~     (p=0.661 n=10+9)

    name       old alloc/op   new alloc/op   delta
    GetList-8    83.3kB ± 1%    67.4kB ± 1%  -19.05%  (p=0.000 n=10+10)

    name       old allocs/op  new allocs/op  delta
    GetList-8     2.89k ± 2%     2.19k ± 1%  -24.24%  (p=0.000 n=10+10)

(real issue is we're calling this code as much as we are, but easy
enough to make it efficient because it'll still need to be called
sometimes in any case)

Updates #5958

Change-Id: I90c20278d73e80315a840aed1397d24faa308d93
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-22 11:30:53 -07:00
Brad Fitzpatrick
7149155b80 portlist: further reduce allocations on Linux
Make Linux parsePorts also an append-style API and attach it to
caller's provided append base memory.

And add a little string intern pool in front of the []byte to string
for inode names.

    name       old time/op    new time/op    delta
    GetList-8    11.1ms ± 4%     9.8ms ± 6%  -11.68%  (p=0.000 n=9+10)

    name       old alloc/op   new alloc/op   delta
    GetList-8    92.8kB ± 2%    79.7kB ± 0%  -14.11%  (p=0.000 n=10+9)

    name       old allocs/op  new allocs/op  delta
    GetList-8     2.94k ± 1%     2.76k ± 0%   -6.16%  (p=0.000 n=10+10)

More coming. (the bulk of the allocations are in addProcesses and
filesystem operations, most of which we should usually be able to
skip)

Updates #5958

Change-Id: I3f0c03646d314a16fef7f8346aefa7d5c96701e7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-22 10:50:51 -07:00
Brad Fitzpatrick
def089f9c9 portlist: unexport all Poller fields, removing unused one, rework channels
Poller.C and Poller.c were duplicated for one caller. Add an accessor
returning the receive-only version instead. It'll inline.

Poller.Err was unused. Remove.

Then Poller is opaque.

The channel usage and shutdown was a bit sketchy. Clean it up.

And document some things.

Change-Id: I5669e54f51a6a13492cf5485c83133bda7ea3ce9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-22 05:47:34 -07:00
Brad Fitzpatrick
46ce80758d portlist: update some internals to use append-style APIs
In prep for reducing garbage, being able to reuse memory.  So far this
doesn't actually reuse much. This is just changing signatures around.

But some improvement in any case:

    bradfitz@tsdev:~/src/tailscale.com$ ~/go/bin/benchstat before after
    name       old time/op    new time/op    delta
    GetList-8    11.8ms ± 9%     9.9ms ± 3%  -15.98%  (p=0.000 n=10+10)

    name       old alloc/op   new alloc/op   delta
    GetList-8    99.5kB ± 2%    91.9kB ± 0%   -7.62%  (p=0.000 n=9+9)

    name       old allocs/op  new allocs/op  delta
    GetList-8     3.05k ± 1%     2.93k ± 0%   -3.83%  (p=0.000 n=8+9)

More later, once parsers can reuse strings from previous parses.

Updates #5958

Change-Id: I76cd5048246dd24d11c4e263d8bb8041747fb2b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-21 22:26:37 -07:00
Brad Fitzpatrick
67597bfc9e portlist: unexport GetList
It's an internal implementation detail, and I plan to refactor it
for performance (garbage) reasons anyway, so start by hiding it.

Updates #5958

Change-Id: I2c0d1f743d3495c5f798d1d8afc364692cd9d290
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-21 20:48:52 -07:00
Will Norris
7b745a1a50 .github: run CI on release branches
Signed-off-by: Will Norris <will@tailscale.com>
2022-10-21 15:18:43 -07:00
Andrew Dunham
74693793be net/netcheck, tailcfg: track whether OS supports IPv6
We had previously added this to the netcheck report in #5087 but never
copied it into the NetInfo struct. Additionally, add it to log lines so
it's visible to support.

Change-Id: Ib6266f7c6aeb2eb2a28922aeafd950fe1bf5627e
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-21 15:31:42 -04:00
Maisem Ali
42d9e7171c Makefile: add publishdevimage target
This builds and publishes the tailscale container image for dev testing.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-21 10:19:06 -07:00
Anton Tolchanov
bd47e28638 prober: optionally spread probes over time
By default all probes with the same probe interval that have been added
together will run on a synchronized schedule, which results in spiky
resource usage and potential throttling by third-party systems (for
example, OCSP servers used by the TLS probes).

To address this, prober can now run in "spread" mode that will
introduce a random delay before the first run of each probe.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-21 09:41:53 +01:00
Charlotte Brandhorst-Satzkorn
adec726fee words: for charlotte, by charlotte (#6002)
What's better than getting a community request?

A community request from another Charlotte!

Bun and hops!

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-10-20 14:50:28 -07:00
Maisem Ali
74637f2c15 wgengine/router: [linux] add before deleting interface addrs
Deleting may temporarily result in no addrs on the interface, which results in
all other rules (like routes) to get dropped by the OS.

I verified this fixes the problem.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-20 13:39:33 -07:00
Charlotte Brandhorst-Satzkorn
95f630ced0 words: beaver, the cutest of them all (#6001)
Beavers have scales, on their tails.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-10-20 12:18:15 -07:00
phirework
d13c9cdfb4 wgengine/magicsock: set up pathfinder (#5994)
Sets up new file for separate silent disco goroutine, tentatively named
pathfinder for now.

Updates #540

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-10-20 14:34:49 -04:00
Brad Fitzpatrick
deac82231c wgengine/magicsock: add start of alternate send path
During development of silent disco (#540), an alternate send policy
for magicsock that doesn't wake up the radio frequently with
heartbeats, we want the old & new policies to coexist, like we did
previously pre- and post-disco.

We started to do that earlier in 5c42990c2f but only set up the
env+control knob plumbing to set a bool about which path should be
used.

This starts to add a way for the silent disco code to update the send
path from a separate goroutine. (Part of the effort is going to
de-state-machinify the event based soup that is the current disco
code and make it more Go synchronous style.)

So far this does nothing. (It does add an atomic load on each send
but that should be noise in the grand scheme of things, and a even more
rare atomic store of nil on node config changes.)

Baby steps.

Updates #540

Co-authored-by: Jenny Zhang <jz@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-20 08:45:42 -07:00
Anton Tolchanov
69f61dcad8 prober: add a DERP probe manager based on derpprobe
This ensures that each DERP server is probed individually (TLS and STUN)
and also manages per-region mesh probing. Actual probing code has been
copied from cmd/derpprobe.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-20 13:54:34 +01:00
Charlotte Brandhorst-Satzkorn
f39847aa52 words: double double tails and trouble, scales aflame and puns abubble (#5992)
Months upon months I ponder about this,
Adding new words onto our little lists.
Given our integrity I should not have missed,
Including the creatures from folklore and myth.
Carefully curated, many of them hiss,
Don't forget about the ones hiding in the abyss.
Now they are added, I cannot resist,
Searching for more words for me to enlist.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-10-19 21:14:55 -07:00
Brad Fitzpatrick
afce773aae ipn: remove handle.go
It was unused in this repo. The Windows client used it, but it can move there.

Change-Id: I572816fd80cbbf1b8db734879b6280857d5bd2a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-19 20:52:22 -07:00
Brad Fitzpatrick
18c61afeb9 types/key: add ChallengePublic, ChallengePrivate, NewChallenge
Updates #5972

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-19 19:17:53 -07:00
Maisem Ali
d0b7a44840 api.md: add expirySeconds as parameter to post Tailnet keys
Updates #4570

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-19 12:05:54 -07:00
Andrew Dunham
e966f024b0 net/dns: print systemd-resolved ResolvConfMode
The ResolvConfMode property is documented to return how systemd-resolved
is currently managing /etc/resolv.conf. Include that information in the
debug line, when available, to assist in debugging DNS issues.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I1ae3a257df1d318d0193a8c7f135c458ec45093e
2022-10-19 11:25:36 -04:00
Andrew Dunham
223126fe5b cmd/derper, net/netcheck: add challenge/response to generate_204 endpoint
The Lufthansa in-flight wifi generates a synthetic 204 response to the
DERP server's /generate_204 endpoint. This PR adds a basic
challenge/response to the endpoint; something sufficiently complicated
that it's unlikely to be implemented by a captive portal. We can then
check for the expected response to verify whether we're being MITM'd.

Follow-up to #5601

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I94a68c9a16a7be7290200eea6a549b64f02ff48f
2022-10-19 11:10:18 -04:00
Anton Tolchanov
d499afac78 net/interfaces: improve default route detection
Instead of treating any interface with a non-ifscope route as a
potential default gateway, now verify that a given route is
actually a default route (0.0.0.0/0 or ::/0).

Fixes #5879

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-19 11:10:19 +01:00
Anton Tolchanov
9c2ad7086c net/interfaces: deduplicate route table parsing on Darwin and FreeBSD
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-19 11:10:19 +01:00
Mihai Parparita
9d04ffc782 net/wsconn: add back custom wrapper for turning a websocket.Conn into a net.Conn
We removed it in #4806 in favor of the built-in functionality from the
nhooyr.io/websocket package. However, it has an issue with deadlines
that has not been fixed yet (see nhooyr/websocket#350). Temporarily
go back to using a custom wrapper (using the fix from our fork) so that
derpers will stop closing connections too aggressively.

Updates #5921

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-18 15:39:32 -07:00
Maya Kaczorowski
d00b095f14 .github: update issue templates (#5978)
Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>

Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2022-10-18 09:00:22 -07:00
Brad Fitzpatrick
9475801ebe ipn/ipnlocal: fix E.G.G. port number accounting
Change-Id: Id35461fdde79448372271ba54f6e6af586f2304d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-18 06:43:47 -07:00
Mihai Parparita
37da617380 .github/workflows: use fast compression for NPM package CI check
Starting with #5946 we're compressing main.wasm when building the
package, but that should not show down the CI check.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-17 15:29:54 -07:00
Mihai Parparita
7741e9feb0 cmd/tsconnect: add progress and connection callbacks
Allows UI to display slightly more fine-grained progress when the SSH
connection is being established.

Updates tailscale/corp#7186

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-17 15:26:59 -07:00
Brad Fitzpatrick
246274b8e9 control/controlhttp: allow setting, getting Upgrade headers in Noise upgrade
Not currently used, but will allow us to usually remove a round-trip for
a future feature.

Updates #5972

Change-Id: I2770ea28e3e6ec9626d1cbb505a38ba51df7fba2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-17 15:11:03 -07:00
License Updater
03ecf335f7 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-17 14:09:10 -07:00
Joe Tsai
14100c0985 wgengine/magicsock: restore allocation-free endpoint.DstToString (#5971)
The wireguard-go code unfortunately calls this unconditionally
even when verbose logging is disabled.

Partial revert of #5911.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-17 13:22:48 -07:00
Brad Fitzpatrick
45b7e8c23c cmd/tailscale: make tailscale cert --serve-demo accept optional listen argument
Change-Id: I48f2f4f74c9996b9ed4bee02c61f125d42154a34
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-17 13:16:41 -07:00
Maisem Ali
630bcb5b67 tsnet,client/tailscale: add APIClient which runs API over Noise.
Updates tailscale/corp#4383

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-17 11:37:17 -07:00
Tom DNetto
e8a11f6181 tka: make rotation signatures use nested keyID
Duplicating this at each layer doesnt make any sense, and is another
invariant where things could go wrong.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-17 10:59:15 -07:00
Xe Iaso
86c5bddce2 tsnet/examples/tshello: update example for LocalClient method (#5966)
Before this would silently fail if this program was running on a machine
that was not already running Tailscale. This patch changes the WhoIs
call to use the tsnet.Server LocalClient instead of the global tailscale
LocalClient.

Signed-off-by: Xe <xe@tailscale.com>

Signed-off-by: Xe <xe@tailscale.com>
2022-10-17 13:43:46 -04:00
Joe Tsai
9116e92718 cmd/netlogfmt: new package to pretty print network traffic logs (#5930)
This package parses a JSON stream of netlog.Message from os.Stdin
and pretty prints the contents as a stream of tables.

It supports reverse lookup of tailscale IP addresses if given
an API key and the tailnet that these traffic logs belong to.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-17 10:36:28 -07:00
Joe Tsai
9ee3df02ee wgengine/magicsock: remove endpoint.wgEndpoint (#5911)
This field seems seldom used and the documentation is wrong.
It is simpler to just derive its original value dynamically
when endpoint.DstToString is called.

This method is potentially used by wireguard-go,
but not in any code path is performance sensitive.
All calls to it use it in conjunction with fmt.Printf,
which is going to be slow anyways since it uses Go reflection.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-17 10:36:08 -07:00
dependabot[bot]
3a33895f1b .github: Bump peter-evans/create-pull-request from 4.1.1 to 4.1.4 (#5965)
* .github: Bump peter-evans/create-pull-request from 4.1.1 to 4.1.4
* Update semantic version comment.

Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.1.1 to 4.1.4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](18f90432be...ad43dccb4d)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M. J. Fromberger <fromberger@tailscale.com>
2022-10-17 09:25:29 -07:00
Andrew Dunham
a4e707bcf0 control/controlhttp: try to avoid flakes in TestDialPlan
Updates tailscale/corp#7446

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ifcf3b5176f065c2e67cbb8943f6356dea720a9c5
2022-10-17 11:34:57 -04:00
Denton Gentry
b55761246b prober: add utilities to generate alerts and warnings.
sendAlert will trigger the Incident Response system.
sendWarning will post to Slack.

Co-authored-by: M. J. Fromberger <fromberger@tailscale.com>
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-16 23:34:04 -07:00
Maisem Ali
af966391c7 kube: handle 201 as a valid status code.
Fixes tailscale/corp#7478

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-16 14:47:27 -07:00
Denton Gentry
19dfdeb1bb cmd/tailscale: correct --cpu-profile help text
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-16 09:07:24 -07:00
Charlotte Brandhorst-Satzkorn
4eed2883db words: space, the final frontier (#5952)
Captains log. Stardate 100386.37.

Work is proceeding on the Words list as Tailscalars are forced to scavenge for more taily and scaley things.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-10-15 12:46:19 -07:00
Andrew Dunham
c32f9f5865 cmd/tailscale, ipn: enable debug logs when --report flag is passed to bugreport (#5830)
Change-Id: Id22e9f4a2dcf35cecb9cd19dd844389e38c922ec
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-15 13:31:35 -04:00
Andrew Dunham
64ea60aaa3 derp: add TCP RTT metric on Linux (#5949)
Periodically poll the TCP RTT metric from all open TCP connections and
update a (bucketed) histogram metric.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6214902196b05bf7829c9d0ea501ce0e13d984cf
2022-10-15 12:57:10 -04:00
Brad Fitzpatrick
a04f1ff9e6 logtail: default to 2s log flush delay on all platforms
Per chat. This is close enough to realtime but massively reduces
number of HTTP requests. (which you can verify with
TS_DEBUG_LOGTAIL_WAKES and watching tailscaled run at start)

By contrast, this is set to 2 minutes on mobile.

Change-Id: Id737c7924d452de5c446df3961f5e94a43a33f1f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-15 09:25:12 -07:00
Mihai Parparita
63ad49890f cmd/tsconnect: pre-compress main.wasm when building the NPM package
This way we can do that once (out of band, in the GitHub action),
instead of increasing the time of each deploy that uses the package.

.wasm is removed from the list of automatically pre-compressed
extensions, an OSS bump and small change on the corp side is needed to
make use of this change.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-14 15:08:06 -07:00
License Updater
899b4cae10 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-14 13:28:10 -07:00
Tom DNetto
a515fc517b ipn/ipnlocal: make tkaSyncIfNeeded exclusive with a mutex
Running corp/ipn#TestNetworkLockE2E has a 1/300 chance of failing, and
deskchecking suggests thats whats happening are two netmaps are racing each
other to be processed through tkaSyncIfNeededLocked. This happens in the
first place because we release b.mu during network RPCs.

To fix this, we make the tka sync logic an exclusive section, so two
netmaps will need to wait for tka sync to complete serially (which is what
we would want anyway, as the second run through probably wont need to
sync).

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-14 12:42:43 -07:00
Tom DNetto
227777154a control/controlclient,ipn/ipnlocal,tailcfg: rotate node-key signature on register
CAPVER 47

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-14 10:23:40 -07:00
Anton Tolchanov
26af329fde prober: expand certificate verification logic in the TLS prober
TLS prober now checks validity period for all server certificates
and verifies OCSP revocation status for the leaf cert.

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-14 15:00:38 +01:00
License Updater
39d03b6b63 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-13 21:34:22 -07:00
Maisem Ali
3555a49518 net/dns: always attempt to read the OS config on macOS/iOS
Also reconfigure DNS on iOS/macOS on link changes.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-13 15:11:07 -07:00
James Tucker
539c073cf0 wgengine/magicsock: set UDP socket buffer sizes to 7MB
- At high data rates more buffer space is required in order to avoid
  packet loss during any cause of delay.
- On slower machines more buffer space is required in order to avoid
  packet loss while decryption & tun writing is underway.
- On higher latency network paths more buffer space is required in order
  to overcome BDP.
- On Linux set with SO_*BUFFORCE to bypass net.core.{r,w}mem_max.
- 7MB is the current default maximum on macOS 12.6
- Windows test is omitted, as Windows does not support getsockopt for
  these options.

Signed-off-by: James Tucker <james@tailscale.com>
2022-10-13 14:46:25 -07:00
Brad Fitzpatrick
a315336287 logtail: change batched upload mechanism to not use CPU when idle
The mobile implementation had a 2 minute ticker going all the time
to do a channel send. Instead, schedule it as needed based on activity.

Then we can be actually idle for long periods of time.

Updates #3363

Change-Id: I0dba4150ea7b94f74382fbd10db54a82f7ef6c29
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-13 14:45:05 -07:00
Will Norris
d05dd41bc1 api.md: document using '-' value as default tailnet
Signed-off-by: Will Norris <will@tailscale.com>
2022-10-13 14:13:28 -07:00
Brad Fitzpatrick
9a264dac01 net/netcheck: fix crash in checkCaptivePortal
If netcheck happens before there's a derpmap.

This seems to only affect Headscale because it doesn't send a derpmap
as early?

Change-Id: I51e0dfca8e40623e04702bc9cc471770ca20d2c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-13 13:09:21 -07:00
Mihai Parparita
b2855cfd86 derp/derphttp: fix nil pointer dereference when closing a netcheck client
NewNetcheckClient only initializes a subset of fields of derphttp.Client,
and the Close() call added by #5707 was result in a nil pointer dereference.
Make Close() safe to call when using NewNetcheckClient() too.

Fixes #5919

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-13 11:49:27 -07:00
James Tucker
4ec6d41682 wgengine/router: fix MTU configuration on Windows
Always set the MTU to the Tailscale default MTU. In practice we are
missing applying an MTU for IPv6 on Windows prior to this patch.

This is the simplest patch to fix the problem, the code in here needs
some more refactoring.

Fixes #5914

Signed-off-by: James Tucker <james@tailscale.com>
2022-10-13 10:48:03 -07:00
Joe Tsai
a1a43ed266 wgengine/netlog: add support for magicsock statistics (#5913)
This sets up Logger to handle statistics at the magicsock layer,
where we can correlate traffic between a particular tailscale IP address
and any number of physical endpoints used to contact the node
that hosts that tailscale address.

We also export Message and TupleCounts to better document the JSON format
that is being sent to the logging infrastructure.

This commit does NOT yet enable the actual logging of magicsock statistics.
That will be a future commit.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-13 10:46:29 -07:00
License Updater
db863bf00f licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-13 07:49:42 -07:00
Joe Tsai
f9120eee57 wgengine: start network logger in Userspace.Reconfig (#5908)
If the wgcfg.Config is specified with network logging arguments,
then Userspace.Reconfig starts up an asynchronous network logger,
which is shutdown either upon Userspace.Close or when Userspace.Reconfig
is called again without network logging or route arguments.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-12 15:05:21 -07:00
Joe Tsai
49bae7fd5c wgengine: fix typo in Engine.PeerForIP (#5912)
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-12 14:14:22 -07:00
Sonia Appasamy
5363a90272 types/view: add ContainsNonExitSubnetRoutes func
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2022-10-12 15:19:36 -05:00
Mihai Parparita
b49eb7d55c cmd/tsconnect: move NPM package to being under the @tailscale org
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-12 13:18:45 -07:00
Joe Tsai
1b4e4cc1e8 wgengine/netlog: new package for traffic flow logging (#5864)
The Logger type managers a logtail.Logger for extracting
statistics from a tstun.Wrapper.
So long as Shutdown is called, it ensures that logtail
and statistic gathering resources are properly cleared up.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-12 11:57:13 -07:00
Brad Fitzpatrick
79755d3ce5 tstest/natlab: add Firewall.Reset method to drop firewall state
For future use in magicsock tests.

Updates #540

Change-Id: I2f07d1a2924f20b36e357c4533ff0a1a974d5061
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-12 10:16:16 -07:00
Denton Gentry
1b9ed9f365 VERSION.txt: this is 1.33.
We did not get this VERSION.txt file checked in at the correct time,
the prior 10 commits in `main` between the v1.32.0 tag point and
this commit were not part of release 1.32. We did no unstable builds
during this time, so the error should have no impact.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-12 09:53:06 -07:00
License Updater
e7519adc18 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-11 15:11:34 -07:00
Brad Fitzpatrick
e24de8a617 ssh/tailssh: add password-forcing workaround for buggy SSH clients
If the username includes a suffix of +password, then we accept
password auth and just let them in like it were no auth.

This exists purely for SSH clients that get confused by seeing success
to their initial auth type "none".

Co-authored-by: Maisem Ali <maisem@tailscale.com>
Change-Id: I616d4c64d042449fb164f615012f3bae246e91ec
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-11 15:03:02 -07:00
Anton Tolchanov
c070d39287 cmd/tailscaled: handle tailscaled symlink on macOS
When Tailscale is installed via Homebrew, `/usr/local/bin/tailscaled`
is a symlink to the actual binary.

Now when `tailscaled install-system-daemon` runs, it will not attempt
to overwrite that symlink if it already points to the tailscaled binary.
However, if executed binary and the link target differ, the path will
he overwritten - this can happen when a user decides to replace
Homebrew-installed tailscaled with a one compiled from source code.

Fixes #5353

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2022-10-11 16:09:26 +01:00
Denton Gentry
51d488673a scripts/installer.sh: add OSMC
Fixes https://github.com/tailscale/tailscale/issues/4960

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-11 04:24:41 -07:00
Emmanuel T Odeke
680f8d9793 all: fix more resource leaks found by staticmajor
Updates #5706

Signed-off-by: Emmanuel T Odeke <emmanuel@orijtech.com>
2022-10-10 20:46:56 -07:00
Brad Fitzpatrick
614a24763b tsweb: sort top-level expvars after removing type prefixes
Fixes #5778

Change-Id: I56c367338fa5686da288cc6545209ef4d6b88549
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-10 20:28:44 -07:00
Brad Fitzpatrick
0475ed4a7e cmd/ssh-auth-none-demo: put the hostname in the package doc
188.166.70.128 port 2222 for now. Some hostname later maybe.

Change-Id: I9c329410035221ed6cdff7a482727d30b77eea8b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-10 10:47:31 -07:00
Maisem Ali
7df85c6031 cmd/ssh-auth-none-demo: add banner as part of the demo
Send two banners with a second in between, this demonstrates the case
where all banners are shown after auth completes and not during.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-10 10:43:31 -07:00
Brad Fitzpatrick
718914b697 tsweb: remove allocs introduced by earlier change
This removes the ~9 allocs added by #5869, while still keeping struct
fields sorted (the previous commit's tests still pass). And add a test
to lock it in that this shouldn't allocate.

Updates #5778

Change-Id: I4c12b9e2a1334adc1ea5aba1777681cb9fc18fbf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-10 10:23:54 -07:00
License Updater
529e893f70 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-09 21:25:19 -07:00
Brad Fitzpatrick
8a187159b2 cmd/ssh-auth-none-demo: add demo SSH server that acts like Tailscale SSH
For SSH client authors to fix their clients without setting up
Tailscale stuff.

Change-Id: I8c7049398512de6cb91c13716d4dcebed4d47b9c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-09 18:07:04 -07:00
Brad Fitzpatrick
b2994568fe ipn/localapi: put all the LocalAPI methods into a map
Rather than a bunch of switch cases.

Change-Id: Id1db813ec255bfab59cbc982bee351eb36373245
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-09 18:05:55 -07:00
Maisem Ali
f172fc42f7 ssh/tailssh: close sshContext on context cancellation
This was preventing tailscaled from shutting down properly if there were
active sessions in certain states (e.g. waiting in check mode).

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-09 17:17:03 -07:00
Hasnain Lakhani
8fe04b035c tsweb: sort varz by name after stripping prefix (#5778)
This makes it easier to view prometheus metrics.

Added a test case which demonstrates the new behavior - the test
initially failed as the output was ordered in the same order
as the fields were declared in the struct (i.e. foo_a, bar_a, foo_b,
bar_b). For that reason, I also had to change an existing test case
to sort the fields in the new expected order.

Signed-off-by: Hasnain Lakhani <m.hasnain.lakhani@gmail.com>
2022-10-09 16:55:51 -07:00
License Updater
d29ec4d7a4 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-09 16:49:03 -07:00
Maisem Ali
4de1601ef4 ssh/tailssh: add support for sending multiple banners
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-09 14:59:48 -07:00
License Updater
91b5c50b43 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-09 11:43:06 -07:00
Maisem Ali
ecf6cdd830 ssh/tailssh: add TestSSHAuthFlow
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-09 10:27:31 -07:00
Maisem Ali
f16b77de5d ssh/tailssh: do the full auth flow during ssh auth
Fixes #5091

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-09 10:27:31 -07:00
License Updater
c8a3d02989 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-09 08:23:16 -07:00
Brad Fitzpatrick
6d76764f37 ipn/ipnlocal: fix taildrop target list UI bug
The macOS and iOS apps that used the /localapi/v0/file-targets handler
were getting too many candidate targets. They wouldn't actually accept
the file. This is effectively just a UI glitch in the wrong hosts
being listed as valid targets from the source side.

Change-Id: I6907a5a1c3c66920e5ec71601c044e722e7cb888
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-07 21:21:23 -07:00
Maisem Ali
b84ec521bf ssh/tailssh: do not send EOT on session disconnection
This was assumed to be the fix for mosh not working, however turns out
all we really needed was the duplicate fd also introduced in the same
commit (af412e8874).

Fixes #5103

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-07 07:52:35 -07:00
Joe Tsai
82f5f438e0 wgengine/wgcfg: plumb down audit log IDs (#5855)
The node and domain audit log IDs are provided in the map response,
but are ultimately going to be used in wgengine since
that's the layer that manages the tstun.Wrapper.

Do the plumbing work to get this field passed down the stack.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-06 16:19:38 -07:00
Mihai Parparita
92ad56ddcb cmd/tsconnect: close the SSH session an unload event instead of beforeunload
The window may not end up getting unloaded (if other beforeunload
handlers prevent the event), thus we should only close the SSH session
if it's truly getting unloaded.

Updates tailscale/corp#7304

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-06 13:17:15 -07:00
Joe Tsai
84e8f25c21 net/tstun: rename statististics method (#5852)
Rename StatisticsEnable as SetStatisticsEnabled to be consistent
with other similarly named methods.

Rename StatisticsExtract as ExtractStatistics to follow
the convention where methods start with a verb.
It was originally named with Statistics as a prefix so that
statistics related methods would sort well in godoc,
but that property no longer holds.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-06 10:46:09 -07:00
Joe Tsai
dd045a3767 net/flowtrack: add json tags to Tuple (#5849)
By convention, JSON serialization uses camelCase.
Specify such names on the Tuple type.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-05 19:40:49 -07:00
Joe Tsai
a73c423c8a net/tunstats: add Counts.Add (#5848)
The Counts.Add method merges two Counts together.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-05 13:18:08 -07:00
Joe Tsai
3af0d4d0f2 logtail: always record timestamps in UTC (#5732)
Upstream optimizations to the Go time package will make
unmarshaling of time.Time 3-6x faster. See:
* https://go.dev/cl/425116
* https://go.dev/cl/425197
* https://go.dev/cl/429862

The last optimization avoids a []byte -> string allocation
if the timestamp string less than than 32B.
Unfortunately, the presence of a timezone breaks that optimization.
Drop recording of timezone as this is non-essential information.

Most of the performance gains is upon unmarshal,
but there is also a slight performance benefit to
not marshaling the timezone as well.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-05 12:27:52 -07:00
Joe Tsai
c321363d2c logtail: support a copy ID (#5851)
The copy ID operates similar to a CC in email where
a message is sent to both the primary ID and also the copy ID.
A given log message is uploaded once, but the log server
records it twice for each ID.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-05 12:25:10 -07:00
Joe Tsai
24ebf161e8 net/tstun: instrument Wrapper with statistics gathering (#5847)
If Wrapper.StatisticsEnable is enabled,
then per-connection counters are maintained.
If enabled, Wrapper.StatisticsExtract must be periodically called
otherwise there is unbounded memory growth.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-05 12:24:30 -07:00
Tom DNetto
a37ee8483f ipn/ipnlocal: fix data race from missing lock in NetworkLockStatus
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-05 11:51:49 -07:00
Brad Fitzpatrick
7714261566 go.toolchain.rev: update to Go 1.19.2
Changes: https://github.com/tailscale/go/commits/build-3fd24dee31726924c1b61c8037a889b30b8aa0f6

Change-Id: I61b83eef2b812879544a5226687606ae792b0786
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-05 11:22:00 -07:00
Tom DNetto
8602061f32 ipn/ipnlocal,tka: Fix bugs found by integration testing
* tka.State.staticValidateCheckpoint could call methods on a contained key prior to calling StaticValidate on that key
 * Remove broken backoff / RPC retry logic from tka methods in ipn/ipnlocal, to be fixed at a later time
 * Fix NetworkLockModify() which would attempt to take b.mu twice and deadlock, remove now-unused dependence on netmap
 * Add methods on ipnlocal.LocalBackend to be used in integration tests
 * Use TAILSCALE_USE_WIP_CODE as the feature flag so it can be manipulated in tests

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-05 11:12:34 -07:00
Tom DNetto
73db56af52 ipn/ipnlocal: filter peers with bad signatures when tka is enabled
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-10-05 10:56:17 -07:00
Kristoffer Dalby
01ebef0f4f tailcfg: add views for ControlDialPlan (#5843) 2022-10-05 16:18:26 +02:00
Will Norris
62bc1052a2 tsweb: allow HTTPError to unwrap errors
Signed-off-by: Will Norris <will@tailscale.com>
2022-10-04 21:15:44 -07:00
License Updater
2243dbccb7 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-04 20:11:30 -07:00
Brad Fitzpatrick
b1bd96f114 go.mod, ssh/tailssh: fix ImplictAuthMethod typo
Fixes #5745

Change-Id: Ie8bc88bd465a9cb35b0ae7782d61ce96480473ee
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-04 19:51:05 -07:00
David Anderson
fde20f3403 cmd/pgproxy: link to blog post at the top.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-10-04 16:47:12 -07:00
Mihai Parparita
7ffd2fe005 cmd/tsconnect: switch to non-beta versions of xterm and related packages
xterm 5.0 was released a few weeks ago, and it picks up
xtermjs/xterm.js#4069, which was the main reason why we were on a 5.0
beta.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-10-04 15:51:36 -07:00
Joe Tsai
2934c5114c net/tunstats: new package to track per-connection counters (#5818)
High-level API:

	type Statistics struct { ... }
	type Counts struct { TxPackets, TxBytes, RxPackets, RxBytes uint64 }
	func (*Statistics) UpdateTx([]byte)
	func (*Statistics) UpdateRx([]byte)
	func (*Statistics) Extract() map[flowtrack.Tuple]Counts

The API accepts a []byte instead of a packet.Parsed so that a future
implementation can directly hash the address and port bytes,
which are contiguous in most IP packets.
This will be useful for a custom concurrent-safe hashmap implementation.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-10-04 15:10:33 -07:00
David Anderson
bdf3d2a63f cmd/pgproxy: open-source our postgres TLS-enforcing proxy.
From the original commit that implemented it:

  It accepts Postgres connections over Tailscale only, dials
  out to the configured upstream database with TLS (using
  strong settings, not the swiss cheese that postgres defaults to),
  and proxies the client through.

  It also keeps an audit log of the sessions it passed through,
  along with the Tailscale-provided machine and user identity
  of the connecting client.

In our other repo, this was:
commit 92e5edf98e8c2be362f564a408939a5fc3f8c539,
Change-Id I742959faaa9c7c302bc312c7dc0d3327e677dc28.

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-10-04 14:54:52 -07:00
License Updater
c5ce355756 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-04 11:09:26 -07:00
Florian Lehner
7e0ffc17fd Address GO-2022-0969
HTTP/2 server connections can hang forever waiting for a clean
shutdown that was preempted by a fatal error. This condition can
be exploited by a malicious client to cause a denial of service.

Signed-off-by: Florian Lehner <dev@der-flo.net>
2022-10-04 11:06:25 -07:00
Florian Lehner
17348915fa Address GO-2020-0042
Due to improper path santization, RPMs containing relative file
paths can cause files to be written (or overwritten) outside of the
target directory.

Signed-off-by: Florian Lehner <dev@der-flo.net>
2022-10-04 11:06:25 -07:00
Brad Fitzpatrick
1841d0bf98 wgengine/magicsock: make debug-level stuff not logged by default
And add a CLI/localapi and c2n mechanism to enable it for a fixed
amount of time.

Updates #1548

Change-Id: I71674aaf959a9c6761ff33bbf4a417ffd42195a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-04 11:05:50 -07:00
Andrew Dunham
5c69961a57 cmd/tailscale/cli: add --record flag to bugreport (#5826)
Change-Id: I02bdc37a5c1a5a5d030c136ec5e84eb4c9ab1752
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-04 14:03:46 -04:00
Andrew Dunham
e5636997c5 wgengine: don't re-allocate trimmedNodes map (#5825)
Change-Id: I512945b662ba952c47309d3bf8a1b243e05a4736
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-10-04 13:20:09 -04:00
License Updater
445c8a4671 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-10-03 12:55:16 -07:00
Andrew Dunham
d7c0410ea8 ipn/localapi: print hostinfo and health on bugreport (#5816)
This information is super helpful when debugging and it'd be nice to not
have to scroll around in the logs to find it near a bugreport.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-10-03 10:54:46 -04:00
Maisem Ali
4102a687e3 tsnet: fix netstack leak on Close
Identified while investigating a goroutine leak in a different repo.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-01 16:44:54 -07:00
Maisem Ali
5fc8843c4c docs/k8s: [proxy] fix sysctl command
Fixes #5805

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-10-01 14:10:05 -07:00
Mihai Parparita
8343b243e7 all: consistently initialize Logf when creating tsdial.Dialers
Most visible when using tsnet.Server, but could have resulted in dropped
messages in a few other places too.

Fixes #5743

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-30 14:40:56 -07:00
Cuong Manh Le
a7efc7bd17 util/singleflight: sync with upstream
Sync with golang.org/x/sync/singleflight at commit
8fcdb60fdcc0539c5e357b2308249e4e752147f1

Fixes #5790

Signed-off-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
2022-09-30 06:55:04 -07:00
Josh Soref
d4811f11a0 all: fix spelling mistakes
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2022-09-29 13:36:13 -07:00
Joe Tsai
e73657d7aa logpolicy: directly expose the logtail server URL (#5788)
Callers of LogHost often jump through hoops to undo the
loss of information dropped by LogHost (e.g., the HTTP scheme).

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-09-29 13:28:51 -07:00
Brad Fitzpatrick
bb7be74756 net/dns/publicdns: permit more NextDNS profile bits in its IPv6 suffix
I brain-o'ed the math earlier. The NextDNS prefix is /32 (actually
/33, but will guarantee last bit is 0), so we have 128-32 = 96 bits
(12 bytes) of config/profile ID that we can extract. NextDNS doesn't
currently use all those, but might.

Updates #2452

Change-Id: I249bd28500c781e45425fd00fd3f46893ae226a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-29 12:23:38 -07:00
Adrian Dewhurst
c581ce7b00 cmd/tailscale, client, ipn, tailcfg: add network lock modify command
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2022-09-29 11:28:47 -07:00
Andrew Dunham
420d841292 wgengine: log subnet router decision at v1 if we have a BIRD client (#5786)
Updates tailscale/coral#82

Change-Id: I398d75f7e178ff7c531ca09899c82cf974fc30c9
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-29 14:14:14 -04:00
Tom DNetto
58ffe928af ipn/ipnlocal, tka: Implement TKA synchronization with the control plane
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-29 11:07:02 -07:00
Tom DNetto
ab591906c8 wgengine/router: Increase range of rule priorities when detecting mwan3
Context: https://github.com/tailscale/tailscale/pull/5588#issuecomment-1260655929

It seems that if the interface at index 1 is down, the rule is not installed. As such,
we increase the range we detect up to 2004 in the hope that at least one of the interfaces
1-4 will be up.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-29 10:09:06 -07:00
Mihai Parparita
9214b293e3 tstime: add ParseDuration helper function
More expressive than time.ParseDuration, also accepting d (days) and
w (weeks) literals.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-28 18:07:27 -07:00
Aaron Klotz
44f13d32d7 cmd/tailscaled, util/winutil: log Windows service diagnostics when the wintun device fails to install
I added new functions to winutil to obtain the state of a service and all
its depedencies, serialize them to JSON, and write them to a Logf.

When tstun.New returns a wrapped ERROR_DEVICE_NOT_AVAILABLE, we know that wintun
installation failed. We then log the service graph rooted at "NetSetupSvc".
We are interested in that specific service because network devices will not
install if that service is not running.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-09-28 16:09:10 -06:00
Brad Fitzpatrick
18159431ab logpolicy: fix, test LogHost to work as documented
Change-Id: I225c9602a7587c69c237e336d0714fc8315ea6bd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-28 14:02:35 -07:00
Andrew Dunham
a5fab23e8f net/dns: format OSConfig correctly with no pointers (#5766)
Fixes tailscale/tailscale#5669

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-27 19:30:39 -04:00
Andrew Dunham
4b996ad5e3 util/deephash: add AppendSum method (#5768)
This method can be used to obtain the hex-formatted deephash.Sum
instance without allocations.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-27 18:22:31 -04:00
Tom DNetto
ebd1637e50 ipn/ipnlocal,tailcfg: Identify client using NodeKey in tka RPCs
Updates https://github.com/tailscale/corp/pull/7024

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-27 09:37:28 -07:00
Mihai Parparita
e97d5634bf control/controlhttp: use custom port for non-localhost JS noise client connections
Control may not be bound to (just) localhost when sharing dev servers,
allow the Wasm client to connect to it in that case too.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-26 17:19:32 -07:00
Emmanuel T Odeke
f981b1d9da all: fix resource leaks with missing .Close() calls
Fixes #5706

Signed-off-by: Emmanuel T Odeke <emmanuel@orijtech.com>
2022-09-26 15:31:54 -07:00
Brad Fitzpatrick
9bdf0cd8cd ipn/ipnlocal: add c2n /debug/{goroutines,prefs,metrics}
* and move goroutine scrubbing code to its own package for reuse
* bump capver to 45

Change-Id: I9b4dfa5af44d2ecada6cc044cd1b5674ee427575
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-26 11:16:38 -07:00
Josh Soref
7686446c60 Drop duplicated $
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2022-09-26 10:10:50 -07:00
Andrew Dunham
b1867457a6 doctor: add package for running in-depth healthchecks; use in bugreport (#5413)
Change-Id: Iaa4e5b021a545447f319cfe8b3da2bd3e5e5782b
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-26 13:07:28 -04:00
Tom DNetto
e3beb4429f tka: Checkpoint every 50 updates
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-26 10:07:14 -07:00
Brad Fitzpatrick
8a0cb4ef20 control/controlclient: fix recent set-dns regression
SetDNS calls were broken by 6d04184325 the other day. Unreleased.

Caught by tests in another repo.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-25 16:23:55 -07:00
Brad Fitzpatrick
fb4e23506f control/controlclient: stop restarting map polls on health change
At some point we started restarting map polls on health change, but we
don't remember why. Maybe it was a desperate workaround for something.
I'm not sure it ever worked.

Rather than have a haunted graveyard, remove it.

In its place, though, and somewhat as a safety backup, send those
updates over the HTTP/2 noise channel if we have one open. Then if
there was a reason that a map poll restart would help we could do it
server-side. But mostly we can gather error stats and show
machine-level health info for debugging.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-24 08:51:34 -07:00
Brad Fitzpatrick
6d04184325 control/controlclient: add a noiseClient.post helper method
In prep for a future change that would've been very copy/paste-y.

And because the set-dns call doesn't currently use a context,
so timeouts/cancelations are plumbed.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-23 13:18:22 -07:00
Will Norris
8c72aabbdf licenses: remove win.md file
This was renamed to windows.md
2022-09-23 11:21:25 -07:00
James Tucker
f7cb535693 net/speedtest: retune to meet iperf on localhost in a VM
- removed some in-flow time calls
- increase buffer size to 2MB to overcome syscall cost
- move relative time computation from record to report time

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-23 10:46:04 -07:00
James Tucker
146f51ce76 net/packet: fix filtering of short IPv4 fragments
The fragment offset is an 8 byte offset rather than a byte offset, so
the short packet limit is now in fragment block size in order to compare
with the offset value.

The packet flags are in the first 3 bits of the flags/frags byte, and
so after conversion to a uint16 little endian value they are at the
start, not the end of the value - the mask for extracting "more
fragments" is adjusted to match this byte.

Extremely short fragments less than 80 bytes are dropped, but fragments
over 80 bytes are now accepted.

Fixes #5727

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-23 10:43:28 -07:00
Mihai Parparita
c66e15772f tsweb: consider 304s as successful for quiet logging
Static resource handlers will generate lots of 304s, which are
effectively successful responses.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-23 10:40:32 -07:00
Andrew Dunham
e1bdbfe710 tailcfg, control/controlhttp, control/controlclient: add ControlDialPlan field (#5648)
* tailcfg, control/controlhttp, control/controlclient: add ControlDialPlan field

This field allows the control server to provide explicit information
about how to connect to it; useful if the client's link status can
change after the initial connection, or if the DNS settings pushed by
the control server break future connections.

Change-Id: I720afe6289ec27d40a41b3dcb310ec45bd7e5f3e
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-23 13:06:55 -04:00
Aaron Klotz
acc7baac6d tailcfg, util/deephash: add DataPlaneAuditLogID to Node and DomainDataPlaneAuditLogID to MapResponse
We're adding two log IDs to facilitate data-plane audit logging: a node-specific
log ID, and a domain-specific log ID.

Updated util/deephash/deephash_test.go with revised expectations for tailcfg.Node.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-09-22 17:18:28 -06:00
Kyle Carberry
91794f6498 wgengine/magicsock: move firstDerp check after nil derpMap check
This fixes a race condition which caused `c.muCond.Broadcast()` to
never fire in the `firstDerp` if block. It resulted in `Close()`
hanging forever.

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2022-09-22 11:54:56 -07:00
Brad Fitzpatrick
2c447de6cc cmd/tailscaled: use explicit equal sign in --port=$PORT in tailscaled.service
Personal preference (so it's obvious it's not a bool flag), but it
also matches the --state= before it.

Bonus: stop allowing PORT to sneak in extra flags to be passed as
their own arguments, as $FOO and ${FOO} expand differently. (${FOO} is
required to concat to strings)

Change-Id: I994626a5663fe0948116b46a971e5eb2c4023216
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-22 11:54:22 -07:00
Anton Schubert
021bedfb89 docker: add ability to use a custom control socket
Signed-off-by: Anton Schubert <anton.schubert@riedel.net>
2022-09-22 08:48:26 -07:00
hlts2
d988c9f098 fix auth key name
Signed-off-by: hlts2 <hiroto.funakoshi.hiroto@gmail.com>
2022-09-22 03:55:05 -07:00
Andrew Dunham
0607832397 wgengine/netstack: always respond to 4via6 echo requests (#5712)
As the comment in the code says, netstack should always respond to ICMP
echo requests to a 4via6 address, even if the netstack instance isn't
normally processing subnet traffic.

Follow-up to #5709

Change-Id: I504d0776c5824071b2a2e0e687bc33e24f6c4746
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-21 18:07:57 -04:00
Will Norris
565dbc599a Revert "licenses: update win/apple licenses"
This reverts commit aadf63da1d.
2022-09-21 14:28:43 -07:00
License Updater
aadf63da1d licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-09-21 14:19:57 -07:00
Maisem Ali
d5781f61a9 ipn/ipnlocal: return usernames when Tailscale SSH is enabled
It was checking if the sshServer was initialized as a proxy, but that
could either not have been initialized yet or Tailscale SSH could have
been disabled after intialized.

Also bump tailcfg.CurrentCapabilityVersion

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-21 14:06:40 -07:00
Mihai Parparita
a7a0baf6b9 cmd/tsconnect: add error callback for SSH sessions
We were just logging them to the console, which is useful for debugging,
but we may want to show them in the UI too.

Updates tailscale/corp#6939

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-21 13:09:53 -07:00
Tom DNetto
e9b98dd2e1 control/controlclient,ipn/ipnlocal: wire tka enable/disable
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-21 12:57:59 -07:00
Andrew Dunham
b9b0bf65a0 wgengine/netstack: handle 4via6 packets when pinging (#5709)
Change-Id: Ib6ebbaa11219fb91b550ed7fc6ede61f83262e89
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-21 14:19:34 -04:00
Andrew Dunham
c6162c2a94 net/netcheck: add check for captive portal (#5593)
This doesn't change any behaviour for now, other than maybe running a
full netcheck more often. The intent is to start gathering data on
captive portals, and additionally, seeing this in the 'tailscale
netcheck' command should provide a bit of additional information to
users.

Updates #1634

Change-Id: I6ba08f9c584dc0200619fa97f9fde1a319f25c76
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-20 15:31:49 -04:00
Brad Fitzpatrick
aa5e494aba tsweb: export go_version in standard expvar vars
For monitoring.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-20 09:59:43 -07:00
Berk D. Demir
ff13c66f55 cmd/tailscale: fix configure-host command for Synology
d5e7e309 changed the `hostinfo.GetVersion` from distro and distro version
to UTS Name Release and moved distribution information under
`hostinfo.Distro*`.

`tailscale configure-host` command implementation for Synology DSM
environments relies on the old semantics of this string for matching DSM
Major version so it's been broken for a few days.

Pull in `hostinfo` and prefix match `hostinfo.DistroVersion` to match
DSM major version.

Signed-off-by: Berk D. Demir <bdd@mindcast.org>
2022-09-19 21:15:21 -07:00
Brad Fitzpatrick
ed248b04a7 cmd/tailscale: remove leftover debug prints from earlier commit
From 6632504f45

Change-Id: If21789232b3ecc14c1639cf87814af6fa73f535f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-19 21:13:56 -07:00
Mihai Parparita
8158dd2edc cmd/tsconnect: allow SSH connection timeout to be overridden
5 seconds may not be enough if we're still loading the derp map and
connecting to a slow machine.

Updates #5693

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-19 18:00:12 -07:00
Maisem Ali
6632504f45 cmd/tailscale/cli: [up] move lose-ssh check after other validations
The check was happening too early and in the case of error would wait 5
s and then error out. This makes it so that it does validations before
the SSH check.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 12:04:14 -07:00
Maisem Ali
054ef4de56 tailcfg: mark CapabilityFileSharingTarget as inter-node
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-19 11:08:34 -07:00
Brad Fitzpatrick
d045462dfb ipn/ipnlocal: add c2n method to get SSH username candidates
For control to fetch a list of Tailscale SSH username candidates to
filter against the Tailnet's SSH policy to present some valid
candidates to a user.

Updates #3802
Updates tailscale/corp#7007

Change-Id: I3dce57b7a35e66891d5e5572e13ae6ef3c898498
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-19 10:37:04 -07:00
Brad Fitzpatrick
d8eb111ac8 .github/workflows: add cross-android
This would've caught the regression from 7c49db02a before it was
submitted so 42f1d92ae0 wouldn't have been necessary to fix it.

Updates #4482

Change-Id: Ia4a9977e21853f68df96f043672c86a86c0181db
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 09:56:19 -07:00
Brad Fitzpatrick
832031d54b wgengine/magicsock: fix recently introduced data race
From 5c42990c2f, not yet released in a stable build.
Caught by existing tests.

Fixes #5685

Change-Id: Ia76bb328809d9644e8b96910767facf627830600
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-18 08:07:57 -07:00
Denton Gentry
42f1d92ae0 net/netns: implement UseSocketMark for Android.
Build fails on Android:
`../../../../go/pkg/mod/tailscale.com@v1.1.1-0.20220916223019-65c24b6334e9/wgengine/magicsock/magicsock_linux.go:133:12: undefined: netns.UseSocketMark`

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-09-17 23:19:24 -07:00
Brad Fitzpatrick
41bb47de0e cmd/tailscaled: respect $PORT on all platforms, not just Linux
Updates #5114

Change-Id: I6c6e28c493d6a026a03088157d08f9fd182ef373
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 12:30:29 -07:00
Brad Fitzpatrick
3562b5bdfa envknob, health: support Synology, show parse errors in status
Updates #5114

Change-Id: I8ac7a22a511f5a7d0dcb8cac470d4a403aa8c817
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-17 08:42:41 -07:00
phirework
5c42990c2f wgengine/magicsock: add client flag and envknob to disable heartbeat (#5638)
Baby steps towards turning off heartbeat pings entirely as per #540.
This doesn't change any current magicsock functionality and requires additional
changes to send/disco paths before the flag can be turned on.

Updates #540

Change-Id: Idc9a72748e74145b068d67e6dd4a4ffe3932efd0
Signed-off-by: Jenny Zhang <jz@tailscale.com>

Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-09-16 23:48:46 -04:00
Brad Fitzpatrick
65c24b6334 envknob: generalize Windows tailscaled-env.txt support
ipnserver previously had support for a Windows-only environment
variable mechanism that further only worked when Windows was running
as a service, not from a console.

But we want it to work from tailscaed too, and we want it to work on
macOS and Synology. So move it to envknob, now that envknob can change
values at runtime post-init.

A future change will wire this up for more platforms, and do something
more for CLI flags like --port, which the bug was originally about.

Updates #5114

Change-Id: I9fd69a9a91bb0f308fc264d4a6c33e0cbe352d71
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 15:30:19 -07:00
Brad Fitzpatrick
4bda41e701 Dockerfile: add test that build-env Alpine version matches go.mod
So things like #5660 don't happen in the future.

Change-Id: I01234f241e297d5b7bdd18da1bb3cc5420ad2225
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 12:19:09 -07:00
Andrew Dunham
9b71008ef2 control/controlhttp: move Dial options into options struct (#5661)
This turns 'dialParams' into something more like net.Dialer, where
configuration fields are public on the struct.

Split out of #5648

Change-Id: I0c56fd151dc5489c3c94fb40d18fd639e06473bc
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-16 15:06:25 -04:00
Luis Peralta
5623ef0271 Update Dockerfile to use golang:1.19-alpine
Tailscale @4a82b31 does not build in the container image due to using golang:1.18 image

Signed-off-by: Luis Peralta <luis.peralta@gmail.com>
2022-09-16 11:40:31 -07:00
Tyler Lee
486eecc063 Switched Secret snippet to match run.sh
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Tyler Lee
b830c9975f Updated secret example in readme to match the sidecar key value
Signed-off-by: Tyler Lee <tyler.lee@radius.ai>
2022-09-16 11:20:33 -07:00
Brad Fitzpatrick
4a82b317b7 ipn/{ipnlocal,localapi}: use strs.CutPrefix, add more domain validation
The GitHub CodeQL scanner flagged the localapi's cert domain usage as a problem
because user input in the URL made it to disk stat checks.

The domain is validated against the ipnstate.Status later, and only
authenticated root/configured users can hit this, but add some
paranoia anyway.

Change-Id: I373ef23832f1d8b3a27208bc811b6588ae5a1ddd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-16 05:52:33 -07:00
Eng Zer Jun
f0347e841f refactor: move from io/ioutil to io and os packages
The io/ioutil package has been deprecated as of Go 1.16 [1]. This commit
replaces the existing io/ioutil functions with their new definitions in
io and os packages.

Reference: https://golang.org/doc/go1.16#ioutil
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2022-09-15 21:45:53 -07:00
Mihai Parparita
027111fb5a derp: update DERP acronym expansion
Makes the package description consistent with other documentation.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 16:08:12 -07:00
Mihai Parparita
1ce0e558a7 cmd/derper, control/controlhttp: disable WebSocket compression
The data that we send over WebSockets is encrypted and thus not
compressible. Additionally, Safari has a broken implementation of compression
(see nhooyr/websocket#218) that makes enabling it actively harmful.

Fixes tailscale/corp#6943

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-15 15:35:49 -07:00
Brad Fitzpatrick
74674b110d envknob: support changing envknobs post-init
Updates #5114

Change-Id: Ia423fc7486e1b3f3180a26308278be0086fae49b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 15:04:02 -07:00
Brad Fitzpatrick
33ee2c058e wgengine: update comments, remove redundant code in forceFullWireguardConfig
Change-Id: I464a0bce36e3a362c7d7ace0e8d2dd77fa825ee2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 13:03:18 -07:00
Brad Fitzpatrick
d34dd43562 ipn/ipnlocal: remove unused envknob
Change-Id: I6d18af2c469eb660e6ca81d1dcc2af33c9e628aa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-15 11:06:15 -07:00
Andrew Dunham
cf61070e26 net/dnscache: add better logging to bootstrap DNS path (#5640)
Change-Id: I4cde3a72e06dac18df856a0cfeac10ab7e3a9108
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-15 10:41:45 -04:00
Kristoffer Dalby
81574a5c8d portlist: normalise space delimited process names (#5634) 2022-09-15 12:17:31 +02:00
Mihai Parparita
9c6bdae556 cmd/tsconnect: use the parent window for beforeunload event listener
The SSH session may be rendered in a different window that the one that
is executing the script.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-14 11:35:13 -07:00
Mihai Parparita
82e82d9b7a net/dns/resolver: remove unused responseTimeout constant
Timeout is now enforced elsewhere, see discussion in https://github.com/tailscale/tailscale/pull/4408#discussion_r970092333.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-13 18:12:11 -07:00
nyghtowl
0f16640546 net/dns: fix fmt error on Revert print
Fixes #5619

Signed-off-by: nyghtowl <warrick@tailscale.com>
2022-09-13 16:36:15 -07:00
Joe Tsai
aa0064db4d logpolicy: add NewWithConfigPath (#5625)
The version.CmdName implementation is buggy such that it does not correctly
identify the binary name if it embeds other go binaries.
For now, add a NewWithConfigPath API that allows the caller to explicitly
specify this information.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-09-13 16:30:40 -07:00
Brad Fitzpatrick
45a3de14a6 cmd/tailscaled, tailcfg, hostinfo: add flag to disable logging + support
As noted in #5617, our documented method of blocking log.tailscale.io
DNS no longer works due to bootstrap DNS.

Instead, provide an explicit flag (--no-logs-no-support) and/or env
variable (TS_NO_LOGS_NO_SUPPORT=true) to explicitly disable logcatcher
uploads. It also sets a bit on Hostinfo to say that the node is in that
mode so we can end any support tickets from such nodes more quickly.

This does not yet provide an easy mechanism for users on some
platforms (such as Windows, macOS, Synology) to set flags/env. On
Linux you'd used /etc/default/tailscaled typically. Making it easier
to set flags for other platforms is tracked in #5114.

Fixes #5617
Fixes tailscale/corp#1475

Change-Id: I72404e1789f9e56ec47f9b7021b44c025f7a373a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-13 11:47:36 -07:00
Tom DNetto
f6da2220d3 wgengine: set fwmark masks in netfilter & ip rules
This change masks the bitspace used when setting and querying the fwmark on packets. This allows
tailscaled to play nicer with other networking software on the host, assuming the other networking
software is also using fwmarks & a different mask.

IPTables / mark module has always supported masks, so this is safe on the netfilter front.

However, busybox only gained support for parsing + setting masks in 1.33.0, so we make sure we
arent such a version before we add the "/<mask>" syntax to an ip rule command.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-13 09:52:26 -07:00
Mihai Parparita
b22b565947 cmd/tsconnect: allow xterm.js terminal options to be passed in
Allows clients to use a custom theme and other xterm.js customization
options.

Fixes #5610

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-12 16:39:02 -07:00
David Anderson
7c49db02a2 wgengine/magicsock: don't use BPF receive when SO_MARK doesn't work.
Fixes #5607

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-09-12 15:05:44 -07:00
Mihai Parparita
c312e0d264 cmd/tsconnect: allow hostname to be specified
The auto-generated hostname is nice as a default, but there are cases
where the client has a more specific name that it can generate.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-12 14:26:50 -07:00
Mihai Parparita
11fcc3a7b0 cmd/tsconnect: fix xterm.js link opening not working when rendered into another window
The default WebLinksAddon handler uses window.open(), but that gets blocked
by the popup blocker when the event being handled is another window. We
instead need to invoke open() on the window that the event was triggered
in.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-12 13:54:27 -07:00
Will Norris
f03a63910d cmd/tailscale: add licenses link to web UI
The `tailscale web` UI is the primary interface for Synology and Home
Assistant users (and perhaps others), so is the logical place to put our
open source license notices.  I don't love adding things to what is
currently a very minimal UI, but I'm not sure of a better option.

Updates tailscale/corp#5780

Signed-off-by: Will Norris <will@tailscale.com>
2022-09-12 12:06:44 -07:00
Brad Fitzpatrick
024257ef5a net/stun: unmap IPv4 addresses in 16 byte STUN replies
Updates #5602

Change-Id: I2276ad2bfb415b9ff52f37444f2a1d74b38543b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-12 12:03:27 -07:00
Andrew Dunham
eb5939289c cmd/derper: add /generate_204 endpoint (#5601)
For captive portal detection.

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-12 13:43:50 -04:00
Brad Fitzpatrick
16939f0d56 hostinfo: detect being run in a container in more ways
Change-Id: I038ff7705ba232e6cf8dcc9775357ef708d43762
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-12 07:02:01 -07:00
Brad Fitzpatrick
d5e7e3093d hostinfo, tailcfg: split Hostinfo.OSVersion into separate fields
Stop jamming everything into one string.

Fixes #5578

Change-Id: I7dec8d6c073bddc7dc5f653e3baf2b4bf6b68378
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-11 21:40:28 -07:00
Brad Fitzpatrick
708b7bff3d net/dns/publicdns: also support NextDNS DoH query parameters
The plan has changed. Doing query parameters rather than path +
heades. NextDNS added support for query parameters.

Updates #2452

Change-Id: I4783c0a06d6af90756d9c80a7512644ba702388c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-11 09:01:03 -07:00
Brad Fitzpatrick
81bc4992f2 net/netns: add TS_FORCE_LINUX_BIND_TO_DEVICE for Linux
For debugging a macOS-specific magicsock issue. macOS runs in
bind-to-interface mode always. This lets me force Linux into the same
mode as macOS, even if the Linux kernel supports SO_MARK, as it
usually does.

Updates #2331 etc

Change-Id: Iac9e4a7429c1781337e716ffc914443b7aa2869d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-10 18:33:30 -07:00
Brad Fitzpatrick
f3ce1e2536 util/mak: deprecate NonNil, add type-safe NonNilSliceForJSON, NonNilMapForJSON
And put the rationale in the name too to save the callers the need for a comment.

Change-Id: I090f51b749a5a0641897ee89a8fb2e2080c8b782
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-10 12:19:22 -07:00
Brad Fitzpatrick
e7376aca25 net/dns/resolver: set DNS-over-HTTPS Accept and User-Agent header on requests
Change-Id: I14b821771681e70405a507f43229c694159265ff
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-10 08:57:26 -07:00
Tom DNetto
ed2b8b3e1d wgengine/router: reduce routing rule priority for openWRT + mwan3
Fixes #3659

Signed-off-by: Tom DNetto <tom@tailscale.com>
Co-authored-by: Ian Foster <ian@vorsk.com>
2022-09-09 18:21:24 -07:00
Brad Fitzpatrick
c14361e70e net/dns/publicdns: support NextDNS DoH URLs with path parameters
Updates #2452

Change-Id: I0f1c34cc1672e87e7efd0adfe4088724dd0de3ed
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-09 14:16:23 -07:00
Mihai Parparita
b302742137 cmd/tsconnect: enable web links addon in the terminal
More user friendly, and as a side-effect we handle SSH check mode better,
since the URL that's output is now clickable.

Fixes #5247

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-09 11:05:01 -07:00
Mihai Parparita
62035d6485 cmd/tsconnect: switch back to public version of xterm npm package
xtermjs/xterm.js#4069 was merged and published (in 5.0.0-beta.58),
no need for the fork added by 01e6565e8a.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-09 10:50:43 -07:00
Brad Fitzpatrick
89fee056d3 cmd/derper: add robots.txt to disallow all
Fixes #5565

Change-Id: I5626ec2116d9be451caef651dc301b7a82e35550
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-09 10:29:46 -07:00
License Updater
3ed366ee1e licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-09-09 09:14:22 -07:00
Brad Fitzpatrick
2aade349fc net/dns, types/dnstypes: update some comments, tests for DoH
Clarify & verify that some DoH URLs can be sent over tailcfg
in some limited cases.

Updates #2452

Change-Id: Ibb25db77788629c315dc26285a1059a763989e24
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-08 17:16:13 -07:00
Brad Fitzpatrick
58abae1f83 net/dns/{publicdns,resolver}: add NextDNS DoH support
NextDNS is unique in that users create accounts and then get
user-specific DNS IPs & DoH URLs.

For DoH, the customer ID is in the URL path.

For IPv6, the IP address includes the customer ID in the lower bits.

For IPv4, there's a fragile "IP linking" mechanism to associate your
public IPv4 with an assigned NextDNS IPv4 and that tuple maps to your
customer ID.

We don't use the IP linking mechanism.

Instead, NextDNS is DoH-only. Which means using NextDNS necessarily
shunts all DNS traffic through 100.100.100.100 (programming the OS to
use 100.100.100.100 as the global resolver) because operating systems
can't usually do DoH themselves.

Once it's in Tailscale's DoH client, we then connect out to the known
NextDNS IPv4/IPv6 anycast addresses.

If the control plane sends the client a NextDNS IPv6 address, we then
map it to the corresponding NextDNS DoH with the same client ID, and
we dial that DoH server using the combination of v4/v6 anycast IPs.

Updates #2452

Change-Id: I3439d798d21d5fc9df5a2701839910f5bef85463
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-08 12:50:32 -07:00
Mihai Parparita
01e6565e8a cmd/tsconnect: temporarily switch to xterm.js fork that handles popup windows
Allows other work to be unblocked while xtermjs/xterm.js#4069 is worked
through.

To enable testing the popup window handling, the standalone app allows
opening of SSH sessions in new windows by holding down the alt key
while pressing the SSH button.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-08 09:30:52 -07:00
Mihai Parparita
2400ba28b1 cmd/tsconnect: handle terminal resizes before the SSH session is created
Store the requested size is a struct field, and use that when actually
creating the SSH session.

Fixes #5567

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-08 09:30:52 -07:00
Brad Fitzpatrick
2266b59446 go.toolchain.rev: bump to Go 1.19.1
See https://github.com/tailscale/go/pull/34

Change-Id: I56806358cd1be4a2b8f509883e47c93083d82bdf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-07 22:13:01 -07:00
Brad Fitzpatrick
ad7546fb9f tailcfg: fix broken test from comment change
Fix broken build from 255c0472fb

"Oh, that's safe to commit because most tests are passing and it's
just a comment change!", I thought, forgetting I'd added a test that
parses its comments.

Change-Id: Iae93d595e06fec48831215a98adbb270f3bfda05
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-07 22:03:38 -07:00
Brad Fitzpatrick
255c0472fb tailcfg: reformat CurrentCapabilityVersion to be a bulleted list
gofmt in 1.19 is now opinionated about structured text formatting in
comments. It did not like our style and kept fighting us whenever we
changed these lines. Give up the fight and be a bulleted list for it.

See:

* https://go.dev/doc/go1.19#go-doc and
* https://go.dev/doc/comment

Updates #4872

Change-Id: Ifae431218471217168c003ab3b4e03c394ca8105
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-07 21:53:16 -07:00
License Updater
c5adc5243c licenses: update win/apple licenses
Signed-off-by: GitHub <noreply@github.com>
2022-09-07 14:28:24 -07:00
Andrew Dunham
c9961b8b95 cmd/derper: filter out useless HTTP error logs (#5563)
These errors aren't actionable and just fill up logs with useless data.
See the following Go issue for more details:
  https://golang.org/issue/26918

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-07 16:31:06 -04:00
License Updater
8fdf137571 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-09-06 19:19:15 -07:00
Colin Adler
9c8bbc7888 wgengine/magicsock: fix panic in http debug server
Fixes an panic in `(*magicsock.Conn).ServeHTTPDebug` when the
`recentPongs` ring buffer for an endpoint wraps around.

Signed-off-by: Colin Adler <colin1adler@gmail.com>
2022-09-06 15:02:07 -07:00
Andrew Dunham
9240f5c1e2 wgengine/netstack: only accept connection after dialing (#5503)
If we accept a forwarded TCP connection before dialing, we can
erroneously signal to a client that we support IPv6 (or IPv4) without
that actually being possible. Instead, we only complete the client's TCP
handshake after we've dialed the outbound connection; if that fails, we
respond with a RST.

Updates #5425 (maybe fixes!)

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-06 16:04:10 -04:00
Mihai Parparita
2f702b150e cmd/tsconnect: add dev-pkg command for two-sided development
Allows imports of the NPM package added by 1a093ef482
to be replaced with import("http://localhost:9090/pkg/pkg.js"), so that
changes can be made in parallel to both the module and code that uses
it (without any need for NPM publishing or even building of the package).

Updates #5415

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-09-06 12:42:58 -07:00
James Tucker
672c2c8de8 wgengine/magicsock: add filter to ignore disco to old/other ports
Incoming disco packets are now dropped unless they match one of the
current bound ports, or have a zero port*.

The BPF filter passes all packets with a disco header to the raw packet
sockets regardless of destination port (in order to avoid needing to
reconfigure BPF on rebind).

If a BPF enabled node has just rebound, due to restart or rebind, it may
receive and reply to disco ping packets destined for ports other than
those which are presently bound. If the pong is accepted, the pinging
node will now assume that it can send WireGuard traffic to the pinged
port - such traffic will not reach the node as it is not destined for a
bound port.

*The zero port is ignored, if received. This is a speculative defense
and would indicate a problem in the receive path, or the BPF filter.
This condition is allowed to pass as it may enable traffic to flow,
however it will also enable problems with the same symptoms this patch
otherwise fixes.

Fixes #5536

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-06 12:25:04 -07:00
James Tucker
be140add75 wgengine/magicsock: fix regression in initial bind for js
1f959edeb0 introduced a regression for JS
where the initial bind no longer occurred at all for JS.

The condition is moved deeper in the call tree to avoid proliferation of
higher level conditions.

Updates #5537

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-06 12:23:44 -07:00
James Tucker
1f959edeb0 wgengine/magicksock: remove nullability of RebindingUDPConns
Both RebindingUDPConns now always exist. the initial bind (which now
just calls rebind) now ensures that bind is called for both, such that
they both at least contain a blockForeverConn. Calling code no longer
needs to assert their state.

Signed-off-by: James Tucker <james@tailscale.com>
2022-09-06 12:08:31 -07:00
Brad Fitzpatrick
56f6fe204b go.mod, wgengine/wgint: bump wireguard-go
For b51010ba13

Change-Id: Ibf767dfad98aef7e9f0505d91c0d26f924e046d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-06 11:34:30 -07:00
Andrew Dunham
f52a659076 net/dnsfallback: allow setting log function (#5550)
This broke a test in corp that enforces we don't use the log package.

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-09-06 11:19:50 -04:00
Andrew Dunham
b8596f2a2f net/dnsfallback: cache most recent DERP map on disk (#5545)
This is especially helpful as we launch newer DERPs over time, and older
clients have progressively out-of-date static DERP maps baked in. After
this, as long as the client has successfully connected once, it'll cache
the most recent DERP map it knows about.

Resolves an in-code comment from @bradfitz

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-05 14:36:30 -04:00
Maisem Ali
060ecb010f docs/k8s: make run.sh handle SIGINT
It was previously using jobcontrol to achieve this, but that apparently
doesn't work when there is no tty. This makes it so that it directly
handles SIGINT and SIGTERM and passes it on to tailscaled. I tested this
works on a Digital Ocean K8s cluster.

Fixes #5512

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-09-04 15:50:02 -07:00
Brad Fitzpatrick
02de34fb10 cmd/derper: add flag to run derper in bootstrap-dns-only mode
Change-Id: Iba128e94464afa605bc9df1f06a91d296380eed0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-03 19:51:00 -07:00
Will Norris
3344c3b89b tsnet: add Server method to listener
Allow callers to verify that a net.Listener is a tsnet.listener by type
asserting against this Server method, as well as providing access to the
underlying Server.

This is initially being added to support the caddy integration in
caddyserver/caddy#5002.

Signed-off-by: Will Norris <will@tailscale.com>
2022-09-02 16:29:49 -07:00
Andrew Dunham
a0bae4dac8 cmd/derper: add support for unpublished bootstrap DNS entries (#5529)
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-09-02 14:48:30 -04:00
Tom DNetto
9132b31e43 tailcfg: refactor/implement wire structs for TKA
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-09-02 10:29:46 -07:00
Kris Brandow
19008a3023 net/dnscache: use net/netip
Removes usage of net.IP and net.IPAddr where possible from net/dnscache.

Fixes #5282

Signed-off-by: Kris Brandow <kris.brandow@gmail.com>
2022-09-01 17:54:19 -04:00
Brad Fitzpatrick
ba3cc08b62 cmd/tailscale/cli: add backwards compatibility 'up' processing for legacy client
Updates tailscale/corp#6781

Change-Id: I843fc810cbec0140d423d65db81e90179d6e0fa5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-01 14:21:48 -07:00
License Updater
d8bfb7543e licenses: update win/apple licenses
Signed-off-by: GitHub <noreply@github.com>
2022-09-01 14:10:55 -07:00
James Tucker
265b008e49 wgengine: fix race on endpoints in getStatus
Signed-off-by: James Tucker <james@tailscale.com>
2022-09-01 10:58:04 -07:00
Bertrand Lorentz
a5ad57472a cli/cert: Fix help message for --key-file
Signed-off-by: Bertrand Lorentz <bertrand.lorentz@gmail.com>
2022-09-01 10:57:00 -07:00
Xe Iaso
3564fd61b5 cmd/gitops-pusher: standardize hujson before posting to validate (#5525)
Apparently the validate route doesn't check content-types or handle
hujson with comments correctly. This patch makes gitops-pusher convert
the hujson to normal json.

Signed-off-by: Xe <xe@tailscale.com>

Signed-off-by: Xe <xe@tailscale.com>
2022-09-01 13:38:32 -04:00
nyghtowl
cfbbcf6d07 cmd/nginx-auth/nginx-auth: update auth to allow for new domains
With MagicDNS GA, we are giving every tailnet a tailnet-<hex>.ts.net name.
We will only parse out if legacy domains include beta.tailscale.net; otherwise,
set tailnet to the full domain format going forward.

Signed-off-by: nyghtowl <warrick@tailscale.com>
2022-08-31 20:18:13 -07:00
License Updater
9c66dce8e0 licenses: update win/apple licenses
Signed-off-by: GitHub <noreply@github.com>
2022-08-31 15:38:41 -07:00
Brad Fitzpatrick
e470893ba0 wgengine/magicsock: use mak in another spot
Change-Id: I0a46d6243371ae6d126005a2bd63820cb2d1db6b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-31 15:30:26 -07:00
Andrew Dunham
c72caa6672 wgengine/magicsock: use AF_PACKET socket + BPF to read disco messages
This is entirely optional (i.e. failing in this code is non-fatal) and
only enabled on Linux for now. Additionally, this new behaviour can be
disabled by setting the TS_DEBUG_DISABLE_AF_PACKET environment variable.

Updates #3824
Replaces #5474

Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-08-31 14:52:31 -07:00
Mihai Parparita
58f35261d0 cmd/tsconnect: remove debugging code
Remove test prefix added to validate the error code from 27f36f77c3.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-08-31 10:46:47 -07:00
Tom DNetto
be95aebabd tka: implement credential signatures (key material delegation)
This will be needed to support preauth-keys with network lock in the future,
so getting the core mechanics out of the way now.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-08-31 10:13:13 -07:00
License Updater
490acdefb6 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-08-31 09:55:41 -07:00
License Updater
84b74825f0 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2022-08-31 08:38:55 -07:00
Brad Fitzpatrick
9bd9f37d29 go.mod: bump wireguard/windows, which moves to using net/netip
Updates #5162

Change-Id: If99a3f0000bce0c01bdf44da1d513f236fd7cdf8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-31 08:36:56 -07:00
Charlotte Brandhorst-Satzkorn
185f2e4768 words: this title should have been a pun, but I chickened out (#5506)
Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-08-31 07:02:49 -07:00
Denton Gentry
53e08bd7ea VERSION.txt: this is 1.31
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-08-31 06:48:24 -07:00
Andrew Dunham
70ed22ccf9 util/uniq: add ModifySliceFunc (#5504)
Follow-up to #5491. This was used in control... oops!

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-08-30 18:51:18 -04:00
Tom DNetto
7ca17b6bdb tka: validate key after UpdateKey before applying state
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-08-30 15:23:30 -07:00
Andrew Dunham
e945d87d76 util/uniq: use generics instead of reflect (#5491)
This takes 75% less time per operation per some benchmarks on my mac.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-08-30 17:56:51 -04:00
Andrew Dunham
1ac4a26fee ipn/localapi: send Tailscale version in ACME User-Agent (#5499)
Requested by a friend at Let's Encrypt.

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-08-30 16:48:59 -04:00
Brad Fitzpatrick
761163815c tailcfg: add Hostinfo.Userspace{,Router} bits
Change-Id: Iad47f904872f2df146c1f63945f79cfddeac7fe8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-30 12:39:03 -07:00
Brad Fitzpatrick
9f6c8517e0 net/dns: set OS DNS to 100.100.100.100 for route-less ExtraRecords [cap 41]
If ExtraRecords (Hosts) are specified without a corresponding split
DNS route and global DNS is specified, then program the host OS DNS to
use 100.100.100.100 so it can blend in those ExtraRecords.

Updates #1543

Change-Id: If49014a5ecc8e38978ff26e54d1f74fe8dbbb9bc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-30 12:36:25 -07:00
Mihai Parparita
27f36f77c3 cmd/tsconnect: output errors to the JS console too
We were just outputting them to the terminal, but that's hard to debug
because we immediately tear down the terminal when getting an error.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-08-30 10:46:44 -07:00
Xe Iaso
122bd667dc cmd/gitops-pusher: be less paranoid about external modifications (#5488)
This makes a "modified externally" error turn into a "modified externally" warning. It means CI won't fail if someone does something manually in the admin console.

Signed-off-by: Xe <xe@tailscale.com>
2022-08-30 09:41:25 -04:00
Joe Tsai
21cd402204 logtail: do not log when backing off (#5485) 2022-08-30 06:21:03 -07:00
Denton Gentry
0ae0439668 docs/k8s: add IPv6 forwarding in proxy.yaml
Fixes https://github.com/tailscale/tailscale/issues/4999

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-08-30 06:03:15 -07:00
Denton Gentry
6dcc6313a6 CI: add go mod tidy workflow
Fixes https://github.com/tailscale/tailscale/issues/4567

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-08-30 06:02:34 -07:00
Denton Gentry
78dbb59a00 CI: make all workflows get Go version from go.mod
The next time we update the toolchain, all of the CI
Actions will automatically use it when go.mod is updated.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-08-30 06:02:14 -07:00
Joe Tsai
7e40071571 util/deephash: handle slice edge-cases (#5471)
It is unclear whether the lack of checking nil-ness of slices
was an oversight or a deliberate feature.
Lacking a comment, the assumption is that this was an oversight.

Also, expand the logic to perform cycle detection for recursive slices.
We do this on a per-element basis since a slice is semantically
equivalent to a list of pointers.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-30 00:33:18 -07:00
James Tucker
90dc0e1702 wgengine: remove unused singleflight group
Signed-off-by: James Tucker <james@tailscale.com>
2022-08-29 18:16:30 -07:00
Mihai Parparita
2c18517121 cmd/tsconnect: add npm publish workflow
Adds an on-demand GitHub Action that publishes the package to the npm
registry (currently under tailscale-connect, will be moved to
@tailscale/connect once we get control of the npm org).

Makes the package.json for the NPM package be dynamically generated to
have the current Tailscale client version.

Updates #5415

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-08-29 18:02:51 -07:00
Andrew Dunham
d6c3588ed3 wgengine/wgcfg: only write peer headers if necessary (#5449)
On sufficiently large tailnets, even writing the peer header (~95 bytes)
can result in a large amount of data that needs to be serialized and
deserialized. Only write headers for peers that need to have their
configuration changed.

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-08-29 20:47:52 -04:00
James Tucker
81dba3738e wgengine: remove all peer status from open timeout diagnostics
Avoid contention from fetching status for all peers, and instead fetch
status for a single peer.

Updates tailscale/coral#72
Signed-off-by: James Tucker <james@tailscale.com>
2022-08-29 15:54:33 -07:00
James Tucker
ad1cc6cff9 wgengine: use Go API rather than UAPI for status
Signed-off-by: James Tucker <james@tailscale.com>
2022-08-29 15:38:16 -07:00
Maisem Ali
68d9d161f4 net/dns: [win] fix regression in disableDynamicUpdate
Somehow I accidentally set the wrong registry value here.
It should be DisableDynamicUpdate=1 and not EnableDNSUpdate=0.

This is a regression from 545639e.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-08-29 15:28:59 -07:00
Brad Fitzpatrick
c66f99fcdc tailcfg, control/controlclient, ipn/ipnlocal: add c2n (control-to-node) system
This lets the control plane can make HTTP requests to nodes.

Then we can use this for future things rather than slapping more stuff
into MapResponse, etc.

Change-Id: Ic802078c50d33653ae1f79d1e5257e7ade4408fd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-29 15:18:40 -07:00
Brad Fitzpatrick
08b3f5f070 wgengine/wgint: add shady temporary package to get at wireguard internals
For #5451

Change-Id: I43482289e323ba9142a446d551ab7a94a467c43a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-29 10:03:51 -07:00
Nahum Shalman
66d7d2549f logger: migrate rusage syscall use to x/sys/unix
This will be helpful for illumos (#697) and should be safe
everywhere else.

Signed-off-by: Nahum Shalman <nahamu@gmail.com>
2022-08-28 08:29:41 -07:00
Nahum Shalman
d20392d413 cmd/tailscale: add emoji for illumos in status subcommand
Signed-off-by: Nahum Shalman <nahamu@gmail.com>
2022-08-28 08:29:31 -07:00
Andrew Dunham
58cc049a9f util/cstruct: add package for decoding padded C structures (#5429)
I was working on my "dump iptables rules using only syscalls" branch and
had a bunch of C structure decoding to do. Rather than manually
calculating the padding or using unsafe trickery to actually cast
variable-length structures to Go types, I'd rather use a helper package
that deals with padding for me.

Padding rules were taken from the following article:
  http://www.catb.org/esr/structure-packing/

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-08-28 11:12:09 -04:00
Andrew Dunham
9b77ac128a wgengine: print in-flight operations on watchdog trigger (#5447)
In addition to printing goroutine stacks, explicitly track all in-flight
operations and print them when the watchdog triggers (along with the
time they were started at). This should make debugging watchdog failures
easier, since we can look at the longest-running operation(s) first.

Signed-off-by: Andrew Dunham <andrew@tailscale.com>

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-08-27 22:06:18 -04:00
Andrew Dunham
e1738ea78e chirp: add a 10s timeout when communicating with BIRD (#5444)
Prior to this change, if BIRD stops responding wgengine.watchdogEngine
will crash tailscaled.

This happens because in wgengine.userspaceEngine, we end up blocking
forever trying to write a request to or read a response from BIRD with
wgLock held, and then future watchdog'd calls will block on acquiring
that mutex until the watchdog kills the process. With the timeout, we at
least get the chance to print an error message and decide whether we
want to crash or not.

Updates tailscale/coral#72

Signed-off-by: Andrew Dunham <andrew@tailscale.com>

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2022-08-27 20:49:31 -04:00
Joe Tsai
9bf13fc3d1 util/deephash: remove getTypeInfo (#5469)
Add a new lookupTypeHasher function that is just a cached front-end
around the makeTypeHasher function.
We do not need to worry about the recursive type cycle issue that
made getTypeInfo more complicated since makeTypeHasher
is not directly recursive. All calls to itself happen lazily
through a sync.Once upon first use.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 17:39:51 -07:00
Joe Tsai
ab7e6f3f11 util/deephash: require pointer in API (#5467)
The entry logic of Hash has extra complexity to make sure
we always have an addressable value on hand.
If not, we heap allocate the input.
For this reason we document that there are performance benefits
to always providing a pointer.
Rather than documenting this, just enforce it through generics.

Also, delete the unused HasherForType function.
It's an interesting use of generics, but not well tested.
We can resurrect it from code history if there's a need for it.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 16:08:31 -07:00
Joe Tsai
c5b1565337 util/deephash: move pointer and interface logic to separate function (#5465)
This helps pprof better identify which Go kinds take the most time
since the kind is always in the function name.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 15:51:34 -07:00
Joe Tsai
d2e2d8438b util/deephash: move map logic to separate function (#5464)
This helps pprof better identify which Go kinds take the most time
since the kind is always in the function name.

There is a minor adjustment where we hash the length of the map
to be more on the cautious side.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 15:49:26 -07:00
Joe Tsai
23c3831ff9 util/deephash: coalesce struct logic (#5466)
Rather than having two copies []fieldInfo,
just maintain one and perform merging in the same pass.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 15:39:46 -07:00
Joe Tsai
296b008b9f util/deephash: move array and slice logic to separate function (#5463)
This helps pprof better identify which Go kinds take the most time
since the kind is always in the function name.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 15:37:36 -07:00
Joe Tsai
31bf3874d6 util/deephash: use unsafe.Pointer instead of reflect.Value (#5459)
Use of reflect.Value.SetXXX panics if the provided argument was
obtained from an unexported struct field.
Instead, pass an unsafe.Pointer around and convert to a
reflect.Value when necessary (i.e., for maps and interfaces).
Converting from unsafe.Pointer to reflect.Value guarantees that
none of the read-only bits will be populated.

When running in race mode, we attach type information to the pointer
so that we can type check every pointer operation.
This also type-checks that direct memory hashing is within
the valid range of a struct value.

We add test cases that previously caused deephash to panic,
but now pass.

Performance:

	name              old time/op    new time/op    delta
	Hash              14.1µs ± 1%    14.1µs ± 1%    ~     (p=0.590 n=10+9)
	HashPacketFilter  2.53µs ± 2%    2.44µs ± 1%  -3.79%  (p=0.000 n=9+10)
	TailcfgNode       1.45µs ± 1%    1.43µs ± 0%  -1.36%  (p=0.000 n=9+9)
	HashArray         318ns ± 2%     318ns ± 2%    ~      (p=0.541 n=10+10)
	HashMapAcyclic    32.9µs ± 1%    31.6µs ± 1%  -4.16%  (p=0.000 n=10+9)

There is a slight performance gain due to the use of unsafe.Pointer
over reflect.Value methods. Also, passing an unsafe.Pointer (1 word)
on the stack is cheaper than passing a reflect.Value (3 words).

Performance gains are diminishing since SHA-256 hashing now dominates the runtime.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 12:30:35 -07:00
Joe Tsai
e0c5ac1f02 util/deephash: add debug printer (#5460)
When built with "deephash_debug", print the set of HashXXX methods.

Example usage:

	$ go test -run=GetTypeHasher/string_slice -tags=deephash_debug
	U64(2)+U64(3)+S("foo")+U64(3)+S("bar")+FIN

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-27 12:14:07 -07:00
666 changed files with 40761 additions and 10505 deletions

View File

@@ -1,12 +1,12 @@
name: Bug report
description: File a bug report
description: File a bug report. If you need help, contact support instead
labels: [needs-triage, bug]
body:
- type: markdown
attributes:
value: |
Please check if your bug is [already filed](https://github.com/tailscale/tailscale/issues).
Have an urgent issue? Let us know by emailing us at <support@tailscale.com>.
Need help with your tailnet? [Contact support](https://tailscale.com/contact/support) instead.
Otherwise, please check if your bug is [already filed](https://github.com/tailscale/tailscale/issues) before filing a new one.
- type: textarea
id: what-happened
attributes:

View File

@@ -5,4 +5,4 @@ contact_links:
about: Contact us for support
- name: Troubleshooting
url: https://tailscale.com/kb/1023/troubleshooting
about: Troubleshoot common issues
about: See the troubleshooting guide for help addressing common issues

View File

@@ -2,7 +2,7 @@ name: CIFuzz
on: [pull_request]
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:

55
.github/workflows/cross-android.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Android-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Android smoke build
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
# some Android breakages early.
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
env:
GOOS: android
GOARCH: arm64
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: macOS build cmd
env:
GOOS: darwin

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: FreeBSD build cmd
env:
GOOS: freebsd

57
.github/workflows/cross-loong64.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Loongnix-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
id: go
- name: Loongnix build cmd
env:
GOOS: linux
GOARCH: loong64
run: go build ./cmd/...
- name: Loongnix build tests
env:
GOOS: linux
GOARCH: loong64
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: OpenBSD build cmd
env:
GOOS: openbsd

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Wasm client build
env:
GOOS: js
@@ -40,7 +40,7 @@ jobs:
# that depend on it.
run: |
./tool/go run ./cmd/tsconnect --fast-compression build
./tool/go run ./cmd/tsconnect build-pkg
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
- uses: k0kubun/action-slack@v2.0.0
with:

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Windows build cmd
env:
GOOS: windows

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -17,13 +18,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v3
go-version-file: go.mod
- name: depaware
run: go run github.com/tailscale/depaware --check

View File

@@ -50,7 +50,7 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@18f90432bedd2afd6a825469ffd38aa24712a91d #v4.1.1
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply@tailscale.com>

View File

@@ -18,16 +18,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: check 'go generate' is clean
run: |
if [[ "${{github.ref}}" == release-branch/* ]]

35
.github/workflows/go_mod_tidy.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: go mod tidy
on:
push:
branches:
- main
pull_request:
branches:
- "*"
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: check 'go mod tidy' is clean
run: |
go mod tidy
echo
echo
git diff --name-only --exit-code || (echo "Please run 'go mod tidy'."; exit 1)

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -17,13 +18,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v3
go-version-file: go.mod
- name: Run license checker
run: ./scripts/check_license_headers.sh .

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Basic build
run: go build ./cmd/...

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -14,21 +15,20 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Basic build
run: go build ./cmd/...
@@ -39,10 +39,6 @@ jobs:
- name: Get QEMU
run: |
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something
# to run Go binaries. 5.2.0 (Debian bullseye) empirically works, and
# use this PPA which brings in a modern qemu.
sudo add-apt-repository -y ppa:jacob/virtualisation
sudo apt-get -y update
sudo apt-get -y install qemu-user

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,16 +20,15 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
go-version-file: go.mod
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Basic build
run: GOARCH=386 go build ./cmd/...

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -16,12 +17,12 @@ jobs:
gofmt:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Check out code
uses: actions/checkout@v3
go-version-file: go.mod
- name: Run gofmt (goimports)
run: go run golang.org/x/tools/cmd/goimports -d --format-only .
- uses: k0kubun/action-slack@v2.0.0

View File

@@ -0,0 +1,30 @@
name: "@tailscale/connect npm publish"
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- name: Build package
# Build with build_dist.sh to ensure that version information is embedded.
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
# also picks up our custom Go toolchain.
run: |
./build_dist.sh tailscale.com/cmd/tsconnect
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public

View File

@@ -4,6 +4,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -13,19 +14,19 @@ jobs:
ubuntu2004-LTS-cloud-base:
runs-on: [ self-hosted, linux, vm ]
if: "!contains(github.event.head_commit.message, '[ci skip]')"
if: "(github.repository == 'tailscale/tailscale') && !contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set GOPATH
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: Checkout Code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Checkout Code
uses: actions/checkout@v3
go-version-file: go.mod
- name: Run VM tests
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004

View File

@@ -7,6 +7,7 @@ on:
pull_request:
branches:
- '*'
- 'release-branch/*'
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
@@ -19,14 +20,13 @@ jobs:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v3
go-version-file: go.mod
- name: Restore Cache
uses: actions/cache@v3

6
.gitignore vendored
View File

@@ -22,3 +22,9 @@ cmd/tailscaled/tailscaled
# direnv config, this may be different for other people so it's probably safer
# to make this nonspecific.
.envrc
# Ignore personal VS Code settings
.vscode/
# Ignore direnv nix-shell environment cache
.direnv/

View File

@@ -32,7 +32,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.18-alpine AS build-env
FROM golang:1.19-alpine AS build-env
WORKDIR /go/src/tailscale
@@ -66,10 +66,12 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.Long=$VERSION_LONG \
-X tailscale.com/version.Short=$VERSION_SHORT \
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.16
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
COPY --from=build-env /go/src/tailscale/docs/k8s/run.sh /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be
# using build_docker.sh which sets an entrypoint for the image.
RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh

View File

@@ -35,6 +35,9 @@ buildlinuxarm:
buildwasm:
GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
buildlinuxloong64:
GOOS=linux GOARCH=loong64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
./build_docker.sh
@@ -54,3 +57,19 @@ pushspk: spk
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
scp tailscale.spk root@${SYNO_HOST}:
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
publishdevimage:
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@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=latest REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator:
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@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=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh

View File

@@ -6,27 +6,41 @@ Private WireGuard® networks made easy
## Overview
This repository contains all the open source Tailscale client code and
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
This repository contains the majority of Tailscale's open source code.
Notably, it includes the `tailscaled` daemon and
the `tailscale` CLI tool. The `tailscaled` daemon runs on Linux, Windows,
[macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees
on FreeBSD and OpenBSD. The Tailscale iOS and Android apps use this repo's
code, but this repo doesn't contain the mobile GUI code.
The Android app is at https://github.com/tailscale/tailscale-android
Other [Tailscale repos](https://github.com/orgs/tailscale/repositories) of note:
The Synology package is at https://github.com/tailscale/tailscale-synology
* the Android app is at https://github.com/tailscale/tailscale-android
* the Synology package is at https://github.com/tailscale/tailscale-synology
* the QNAP package is at https://github.com/tailscale/tailscale-qpkg
* the Chocolatey packaging is at https://github.com/tailscale/tailscale-chocolatey
For background on which parts of Tailscale are open source and why,
see [https://tailscale.com/opensource/](https://tailscale.com/opensource/).
## Using
We serve packages for a variety of distros at
https://pkgs.tailscale.com .
We serve packages for a variety of distros and platforms at
[https://pkgs.tailscale.com](https://pkgs.tailscale.com/).
## Other clients
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
use the code in this repository but additionally include small GUI
wrappers that are not open source.
wrappers. The GUI wrappers on non-open source platforms are themselves
not open source.
## Building
We always require the latest Go release, currently Go 1.19. (While we build
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
required.)
```
go install tailscale.com/cmd/tailscale{,d}
```
@@ -43,8 +57,6 @@ If your distro has conventions that preclude the use of
`build_dist.sh`, please do the equivalent of what it does in your
distro's way, so that bug reports contain useful version information.
We require the latest Go release, currently Go 1.19.
## Bugs
Please file any issues about this code or the hosted service on
@@ -59,6 +71,9 @@ We require [Developer Certificate of
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
`Signed-off-by` lines in commits.
See `git log` for our commit message style. It's basically the same as
[Go's style](https://github.com/golang/go/wiki/CommitMessage).
## About Us
[Tailscale](https://tailscale.com/) is primarily developed by the

View File

@@ -1 +1 @@
1.29.0
1.35.0

92
api.md
View File

@@ -3,7 +3,7 @@
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
# Authentication
Currently based on {some authentication method}. Visit the [admin panel](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
Currently based on {some authentication method}. Visit the [admin console](https://login.tailscale.com/admin) and navigate to the `Settings` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints (leave the password blank).
# APIs
@@ -335,11 +335,12 @@ The response is 2xx on success. The response body is currently an empty JSON
object.
## Tailnet
A tailnet is the name of your Tailscale network.
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
A tailnet is your private network, composed of all the devices on it and their configuration. For more information on tailnets, see [our user-facing documentation](https://tailscale.com/kb/1136/tailnet/).
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
When making API requests, your tailnet is identified by the organization name. You can find it on the [Settings page](https://login.tailscale.com/admin/settings) of the admin console.
For example, if `alice@example.com` belongs to the `example.com` tailnet, they would use the following format for API calls:
```
GET /api/v2/tailnet/example.com/...
@@ -355,9 +356,14 @@ GET /api/v2/tailnet/alice@gmail.com/...
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
```
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
Alternatively, you can specify the value "-" to refer to the default tailnet of
the authenticated user making the API call. For example:
```
GET /api/v2/tailnet/-/...
curl https://api.tailscale.com/api/v2/tailnet/-/...
```
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
### ACL
@@ -396,20 +402,20 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
// Example/default ACLs for unrestricted connections.
{
"Tests": [],
"tests": [],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"groups": {
"group:example": [
"user1@example.com",
"user2@example.com"
],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
"acls": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{
@@ -479,6 +485,8 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
###### Headers
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
A special value `ts-default` will ensure that ACL will be set only if current ACL is the default one (created automatically for each tailnet).
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
###### POST Body
@@ -486,11 +494,14 @@ Returns the updated ACL in JSON or HuJSON according to the `Accept` header on su
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
An ACL policy may contain the following top-level properties:
* `Groups` - Static groups of users which can be used for ACL rules.
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
* `ACLs` - Access control lists.
* `TagOwners` - Defines who is allowed to use which tags.
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
* `groups` - Static groups of users which can be used for ACL rules.
* `hosts` - Hostname aliases to use in place of IP addresses or subnets.
* `acls` - Access control lists.
* `tagOwners` - Defines who is allowed to use which tags.
* `tests` - Run on ACL updates to check correct functionality of defined ACLs.
* `autoApprovers` - Defines which users can advertise routes or exit nodes without further approval.
* `ssh` - Configures access policy for Tailscale SSH.
* `nodeAttrs` - Defines which devices can use certain features.
See https://tailscale.com/kb/1018/acls for more information on those properties.
@@ -503,22 +514,22 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
--data-binary '// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
"tests": [
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
"acls": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
]
}'
```
@@ -528,22 +539,22 @@ Response:
// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
"tests": [
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
"acls": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
]
}
```
@@ -586,22 +597,22 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/preview?previewFo
--data-binary '// Example/default ACLs for unrestricted connections.
{
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
"Tests": [
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
"tests": [
// {"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]},
],
// Declare static groups of users beyond those in the identity service.
"Groups": {
"groups": {
"group:example": [ "user1@example.com", "user2@example.com" ],
},
// Declare convenient hostname aliases to use in place of IP addresses.
"Hosts": {
"hosts": {
"example-host-1": "100.100.100.100",
},
// Access control lists.
"ACLs": [
"acls": [
// Match absolutely everything. Comment out this section if you want
// to define specific ACL restrictions.
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
{ "action": "accept", "users": ["*"], "ports": ["*:*"] },
]
}'
```
@@ -637,7 +648,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
-u "tskey-yourapikey123:" \
--data-binary '
[
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
{"src": "user1@example.com", "accept": ["example-host-1:22"], "deny": ["example-host-2:100"]}
]'
```
@@ -648,10 +659,10 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
-u "tskey-yourapikey123:" \
--data-binary '
{
"ACLs": [
{ "Action": "accept", "src": ["100.105.106.107"], "dst": ["1.2.3.4:*"] },
"acls": [
{ "action": "accept", "src": ["100.105.106.107"], "dst": ["1.2.3.4:*"] },
],
"Tests", [
"tests", [
{"src": "100.105.106.107", "allow": ["1.2.3.4:80"]}
],
}'
@@ -813,6 +824,10 @@ Supply the tailnet in the path.
###### POST Body
`capabilities` - A mapping of resources to permissible actions.
`expirySeconds` - (Optional) How long the key is valid for in seconds.
Defaults to 90d.
```
{
"capabilities": {
@@ -826,7 +841,8 @@ Supply the tailnet in the path.
]
}
}
}
},
"expirySeconds": 1440
}
```

View File

@@ -9,7 +9,6 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -18,7 +17,7 @@ import (
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}

View File

@@ -54,7 +54,7 @@ while [ "$#" -gt 1 ]; do
--extra-small)
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap"
;;
--box)
shift

View File

@@ -26,23 +26,46 @@ eval $(./build_dist.sh shellvars)
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.16"
DEFAULT_TARGET="client"
PUSH="${PUSH:-false}"
REPOS="${REPOS:-${DEFAULT_REPOS}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
go run github.com/tailscale/mkctr \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--files="docs/k8s/run.sh:/tailscale/run.sh" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
/bin/sh /tailscale/run.sh
case "$TARGET" in
client)
go run github.com/tailscale/mkctr \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled, \
tailscale.com/cmd/containerboot:/usr/local/bin/containerboot" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
/usr/local/bin/containerboot
;;
operator)
go run github.com/tailscale/mkctr \
--gopaths="tailscale.com/cmd/k8s-operator:/usr/local/bin/operator" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
/usr/local/bin/operator
;;
*)
echo "unknown target: $TARGET"
exit 1
;;
esac

View File

@@ -11,15 +11,37 @@ import (
"fmt"
"net"
"strings"
"time"
)
const (
// Maximum amount of time we should wait when reading a response from BIRD.
responseTimeout = 10 * time.Second
)
// New creates a BIRDClient.
func New(socket string) (*BIRDClient, error) {
return newWithTimeout(socket, responseTimeout)
}
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
}
b := &BIRDClient{socket: socket, conn: conn, scanner: bufio.NewScanner(conn)}
defer func() {
if err != nil {
conn.Close()
}
}()
b := &BIRDClient{
socket: socket,
conn: conn,
scanner: bufio.NewScanner(conn),
timeNow: time.Now,
timeout: timeout,
}
// Read and discard the first line as that is the welcome message.
if _, err := b.readResponse(); err != nil {
return nil, err
@@ -32,6 +54,8 @@ type BIRDClient struct {
socket string
conn net.Conn
scanner *bufio.Scanner
timeNow func() time.Time
timeout time.Duration
}
// Close closes the underlying connection to BIRD.
@@ -81,10 +105,15 @@ func (b *BIRDClient) EnableProtocol(protocol string) error {
// 1 means table entry, 8 runtime error and 9 syntax error.
func (b *BIRDClient) exec(cmd string, args ...any) (string, error) {
if err := b.conn.SetWriteDeadline(b.timeNow().Add(b.timeout)); err != nil {
return "", err
}
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
return "", err
}
fmt.Fprintln(b.conn)
if _, err := fmt.Fprintln(b.conn); err != nil {
return "", err
}
return b.readResponse()
}
@@ -105,14 +134,20 @@ func hasResponseCode(s []byte) bool {
}
func (b *BIRDClient) readResponse() (string, error) {
// Set the read timeout before we start reading anything.
if err := b.conn.SetReadDeadline(b.timeNow().Add(b.timeout)); err != nil {
return "", err
}
var resp strings.Builder
var done bool
for !done {
if !b.scanner.Scan() {
return "", fmt.Errorf("reading response from bird failed: %q", resp.String())
}
if err := b.scanner.Err(); err != nil {
return "", err
if err := b.scanner.Err(); err != nil {
return "", err
}
return "", fmt.Errorf("reading response from bird failed (EOF): %q", resp.String())
}
out := b.scanner.Bytes()
if _, err := resp.Write(out); err != nil {

View File

@@ -8,9 +8,12 @@ import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
type fakeBIRD struct {
@@ -103,9 +106,88 @@ func TestChirp(t *testing.T) {
t.Fatal(err)
}
if err := c.EnableProtocol("rando"); err == nil {
t.Fatalf("enabling %q succeded", "rando")
t.Fatalf("enabling %q succeeded", "rando")
}
if err := c.DisableProtocol("rando"); err == nil {
t.Fatalf("disabling %q succeded", "rando")
t.Fatalf("disabling %q succeeded", "rando")
}
}
type hangingListener struct {
net.Listener
t *testing.T
done chan struct{}
wg sync.WaitGroup
sock string
}
func newHangingListener(t *testing.T) *hangingListener {
sock := filepath.Join(t.TempDir(), "sock")
l, err := net.Listen("unix", sock)
if err != nil {
t.Fatal(err)
}
return &hangingListener{
Listener: l,
t: t,
done: make(chan struct{}),
sock: sock,
}
}
func (hl *hangingListener) Stop() {
hl.Close()
close(hl.done)
hl.wg.Wait()
}
func (hl *hangingListener) listen() error {
for {
c, err := hl.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
hl.wg.Add(1)
go hl.handle(c)
}
}
func (hl *hangingListener) handle(c net.Conn) {
defer hl.wg.Done()
// Write our fake first line of response so that we get into the read loop
fmt.Fprintln(c, "0001 BIRD 2.0.8 ready.")
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
hl.t.Logf("connection still hanging")
case <-hl.done:
return
}
}
}
func TestChirpTimeout(t *testing.T) {
fb := newHangingListener(t)
defer fb.Stop()
go fb.listen()
c, err := newWithTimeout(fb.sock, 500*time.Millisecond)
if err != nil {
t.Fatal(err)
}
err = c.EnableProtocol("tailscale")
if err == nil {
t.Fatal("got err=nil, want timeout")
}
if !os.IsTimeout(err) {
t.Fatalf("got err=%v, want os.IsTimeout(err)=true", err)
}
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale
@@ -459,7 +458,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode)
return nil, fmt.Errorf("control api responded with %d status code", resp.StatusCode)
}
// The test ran without fail

View File

@@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package apitype contains types for the Tailscale local API and control plane API.
// Package apitype contains types for the Tailscale LocalAPI and control plane API.
package apitype
import "tailscale.com/tailcfg"
// LocalAPIHost is the Host header value used by the LocalAPI.
const LocalAPIHost = "local-tailscaled.sock"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *tailcfg.Node
@@ -21,7 +24,7 @@ type WhoIsResponse struct {
type FileTarget struct {
Node *tailcfg.Node
// PeerAPI is the http://ip:port URL base of the node's peer API,
// PeerAPI is the http://ip:port URL base of the node's PeerAPI,
// without any path (not even a single slash).
PeerAPIURL string
}

View File

@@ -11,7 +11,6 @@ type DNSConfig struct {
Domains []string `json:"domains"`
Nameservers []string `json:"nameservers"`
Proxied bool `json:"proxied"`
PerDomain bool `json:",omitempty"`
}
type DNSResolver struct {

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale

147
client/tailscale/keys.go Normal file
View File

@@ -0,0 +1,147 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Key represents a Tailscale API or auth key.
type Key struct {
ID string `json:"id"`
Created time.Time `json:"created"`
Expires time.Time `json:"expires"`
Capabilities KeyCapabilities `json:"capabilities"`
}
// KeyCapabilities are the capabilities of a Key.
type KeyCapabilities struct {
Devices KeyDeviceCapabilities `json:"devices,omitempty"`
}
// KeyDeviceCapabilities are the device-related capabilities of a Key.
type KeyDeviceCapabilities struct {
Create KeyDeviceCreateCapabilities `json:"create"`
}
// KeyDeviceCreateCapabilities are the device creation capabilities of a Key.
type KeyDeviceCreateCapabilities struct {
Reusable bool `json:"reusable"`
Ephemeral bool `json:"ephemeral"`
Preauthorized bool `json:"preauthorized"`
Tags []string `json:"tags,omitempty"`
}
// Keys returns the list of keys for the current user.
func (c *Client) Keys(ctx context.Context) ([]string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
var keys []struct {
ID string `json:"id"`
}
if err := json.Unmarshal(b, &keys); err != nil {
return nil, err
}
ret := make([]string, 0, len(keys))
for _, k := range keys {
ret = append(ret, k.ID)
}
return ret, nil
}
// CreateKey creates a new key for the current user. Currently, only auth keys
// can be created. Returns the key itself, which cannot be retrieved again
// later, and the key metadata.
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (string, *Key, error) {
keyRequest := struct {
Capabilities KeyCapabilities `json:"capabilities"`
}{caps}
bs, err := json.Marshal(keyRequest)
if err != nil {
return "", nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
if err != nil {
return "", nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return "", nil, err
}
if resp.StatusCode != http.StatusOK {
return "", nil, handleErrorResponse(b, resp)
}
var key struct {
Key
Secret string `json:"key"`
}
if err := json.Unmarshal(b, &key); err != nil {
return "", nil, err
}
return key.Secret, &key.Key, nil
}
// Key returns the metadata for the given key ID. Currently, capabilities are
// only returned for auth keys, API keys only return general metadata.
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
var key Key
if err := json.Unmarshal(b, &key); err != nil {
return nil, err
}
return &key, nil
}
// DeleteKey deletes the key with the given ID.
func (c *Client) DeleteKey(ctx context.Context, id string) error {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
if err != nil {
return err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale
@@ -15,7 +14,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
@@ -30,6 +28,7 @@ import (
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
@@ -37,13 +36,14 @@ import (
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
)
// defaultLocalClient is the default LocalClient when using the legacy
// package-level functions.
var defaultLocalClient LocalClient
// LocalClient is a client to Tailscale's "local API", communicating with the
// LocalClient is a client to Tailscale's "LocalAPI", communicating with the
// Tailscale daemon on the local machine. Its API is not necessarily stable and
// subject to changes between releases. Some API calls have stricter
// compatibility guarantees, once they've been widely adopted. See method docs
@@ -101,9 +101,6 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
}
@@ -133,11 +130,11 @@ func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error)
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := lc.DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != ipn.IPCVersion() && onVersionMismatch != nil {
onVersionMismatch(ipn.IPCVersion(), server)
if server := res.Header.Get("Tailscale-Version"); server != "" && server != envknob.IPCVersion() && onVersionMismatch != nil {
onVersionMismatch(envknob.IPCVersion(), server)
}
if res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
all, _ := io.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
@@ -198,7 +195,10 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if jr, ok := body.(jsonReader); ok && jr.err != nil {
return nil, jr.err // fail early if there was a JSON marshaling error
}
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
if err != nil {
return nil, err
}
@@ -207,7 +207,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
slurp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
@@ -229,20 +229,21 @@ func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, erro
return defaultLocalClient.WhoIs(ctx, remoteAddr)
}
func decodeJSON[T any](b []byte) (ret T, err error) {
if err := json.Unmarshal(b, &ret); err != nil {
var zero T
return zero, fmt.Errorf("failed to unmarshal JSON into %T: %w", ret, err)
}
return ret, nil
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
return nil, err
}
r := new(apitype.WhoIsResponse)
if err := json.Unmarshal(body, r); err != nil {
if max := 200; len(body) > max {
body = append(body[:max], "..."...)
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
}
return r, nil
return decodeJSON[*apitype.WhoIsResponse](body)
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
@@ -256,8 +257,8 @@ func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/metrics")
}
// Profile returns a pprof profile of the Tailscale daemon.
func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) ([]byte, error) {
// Pprof returns a pprof profile of the Tailscale daemon.
func (lc *LocalClient) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
@@ -265,18 +266,80 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
if sec != 0 || pprofType == "profile" {
secArg = fmt.Sprint(sec)
}
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/pprof?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
// BugReportOpts contains options to pass to the Tailscale daemon when
// generating a bug report.
type BugReportOpts struct {
// Note contains an optional user-provided note to add to the logs.
Note string
// Diagnose specifies whether to print additional diagnostic information to
// the logs when generating this bugreport.
Diagnose bool
// Record specifies, if non-nil, whether to perform a bugreport
// "recording"generating an initial log marker, then waiting for
// this channel to be closed before finishing the request, which
// generates another log marker.
Record <-chan struct{}
}
// BugReportWithOpts logs and returns a log marker that can be shared by the
// user with support.
//
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
qparams := make(url.Values)
if opts.Note != "" {
qparams.Set("note", opts.Note)
}
if opts.Diagnose {
qparams.Set("diagnose", "true")
}
if opts.Record != nil {
qparams.Set("record", "true")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var requestBody io.Reader
if opts.Record != nil {
pr, pw := io.Pipe()
requestBody = pr
// This goroutine waits for the 'Record' channel to be closed,
// and then closes the write end of our pipe to unblock the
// reader.
go func() {
defer pw.Close()
select {
case <-opts.Record:
case <-ctx.Done():
}
}()
}
// lc.send might block if opts.Record != nil; see above.
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// BugReport logs and returns a log marker that can be shared by the user with support.
//
// This is the same as calling BugReportWithOpts and only specifying the Note
// field.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
@@ -287,6 +350,41 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/dev-set-state-store?"+(url.Values{
"key": {key},
"value": {value},
}).Encode(), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// SetComponentDebugLogging sets component's debug logging enabled for
// the provided duration. If the duration is in the past, the debug logging
// is disabled.
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
var res struct {
Error string
}
if err := json.Unmarshal(body, &res); err != nil {
return err
}
if res.Error != "" {
return errors.New(res.Error)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
@@ -312,11 +410,7 @@ func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstat
if err != nil {
return nil, err
}
st := new(ipnstate.Status)
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
return decodeJSON[*ipnstate.Status](body)
}
// IDToken is a request to get an OIDC ID token for an audience.
@@ -327,23 +421,27 @@ func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenR
if err != nil {
return nil, err
}
tr := new(tailcfg.TokenResponse)
if err := json.Unmarshal(body, tr); err != nil {
return nil, err
}
return tr, nil
return decodeJSON[*tailcfg.TokenResponse](body)
}
// WaitingFiles returns the list of received Taildrop files that have been
// received by the Tailscale daemon in its staging/cache directory but not yet
// transferred by the user's CLI or GUI client and written to a user's home
// directory somewhere.
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := lc.get200(ctx, "/localapi/v0/files/")
return lc.AwaitWaitingFiles(ctx, 0)
}
// AwaitWaitingFiles is like WaitingFiles but takes a duration to await for an answer.
// If the duration is 0, it will return immediately. The duration is respected at second
// granularity only. If no files are available, it returns (nil, nil).
func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
path := "/localapi/v0/files/?waitsec=" + fmt.Sprint(int(d.Seconds()))
body, err := lc.get200(ctx, path)
if err != nil {
return nil, err
}
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
return decodeJSON[[]apitype.WaitingFile](body)
}
func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
@@ -352,7 +450,7 @@ func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) e
}
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
}
@@ -365,7 +463,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
@@ -377,11 +475,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
if err != nil {
return nil, err
}
var fts []apitype.FileTarget
if err := json.Unmarshal(body, &fts); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
return fts, nil
return decodeJSON[[]apitype.FileTarget](body)
}
// PushFile sends Taildrop file r to target.
@@ -389,7 +483,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
}
@@ -435,11 +529,7 @@ func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary.
func (lc *LocalClient) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
pj, err := json.Marshal(p)
if err != nil {
return err
}
_, err = lc.send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
_, err := lc.send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, jsonBody(p))
return err
}
@@ -456,21 +546,27 @@ func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
}
func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, jsonBody(mp))
if err != nil {
return nil, err
}
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
return decodeJSON[*ipn.Prefs](body)
}
// StartLoginInteractive starts an interactive login.
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
return err
}
// Start applies the configuration specified in opts, and starts the
// state machine.
func (lc *LocalClient) Start(ctx context.Context, opts ipn.Options) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/start", http.StatusNoContent, jsonBody(opts))
return err
}
// Logout logs out the current node.
func (lc *LocalClient) Logout(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
@@ -512,7 +608,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
},
}
ctx = httptrace.WithClientTrace(ctx, &trace)
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/dial", nil)
if err != nil {
return nil, err
}
@@ -643,14 +739,14 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
@@ -674,11 +770,7 @@ func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
pr := new(ipnstate.PingResult)
if err := json.Unmarshal(body, pr); err != nil {
return nil, err
}
return pr, nil
return decodeJSON[*ipnstate.PingResult](body)
}
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
@@ -687,21 +779,21 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
pr := new(ipnstate.NetworkLockStatus)
if err := json.Unmarshal(body, pr); err != nil {
return nil, err
}
return pr, nil
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockInit initializes the tailnet key authority.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ipnstate.NetworkLockStatus, error) {
//
// TODO(tom): Plumb through disablement secrets.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type initRequest struct {
Keys []tka.Key
Keys []tka.Key
DisablementValues [][]byte
SupportDisablement []byte
}
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys}); err != nil {
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
return nil, err
}
@@ -709,12 +801,105 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ip
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
pr := new(ipnstate.NetworkLockStatus)
if err := json.Unmarshal(body, pr); err != nil {
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
v.Set("limit", fmt.Sprint(maxEntries))
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
}
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
// This endpoint expects an empty JSON stanza as the payload.
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config))
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/serve-config", 200, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
return getServeConfigFromJSON(body)
}
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
if err := json.Unmarshal(body, &sc); err != nil {
return nil, err
}
return pr, nil
return sc, nil
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
@@ -746,3 +931,143 @@ func tailscaledConnectHint() string {
}
return "not running?"
}
type jsonReader struct {
b *bytes.Reader
err error // sticky JSON marshal error, if any
}
// jsonBody returns an io.Reader that marshals v as JSON and then reads it.
func jsonBody(v any) jsonReader {
b, err := json.Marshal(v)
if err != nil {
return jsonReader{err: err}
}
return jsonReader{b: bytes.NewReader(b)}
}
func (r jsonReader) Read(p []byte) (n int, err error) {
if r.err != nil {
return 0, r.err
}
return r.b.Read(p)
}
// ProfileStatus returns the current profile and the list of all profiles.
func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProfile, all []ipn.LoginProfile, err error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/profiles/current", 200, nil)
if err != nil {
return
}
current, err = decodeJSON[ipn.LoginProfile](body)
if err != nil {
return
}
body, err = lc.send(ctx, "GET", "/localapi/v0/profiles/", 200, nil)
if err != nil {
return
}
all, err = decodeJSON[[]ipn.LoginProfile](body)
return current, all, err
}
// 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.
func (lc *LocalClient) SwitchToEmptyProfile(ctx context.Context) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/profiles/", http.StatusCreated, nil)
return err
}
// SwitchProfile switches to the given profile.
func (lc *LocalClient) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/profiles/"+url.PathEscape(string(profile)), 204, nil)
return err
}
// DeleteProfile removes the profile with the given ID.
// If the profile is the current profile, an empty profile
// will be selected as if SwitchToEmptyProfile was called.
func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
return err
}
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
v := url.Values{"region": {regionIDOrCode}}
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[*ipnstate.DebugDERPRegionReport](body)
}
// WatchIPNBus subscribes to the IPN notification bus. It returns a watcher
// once the bus is connected successfully.
//
// The context is used for the life of the watch, not just the call to
// WatchIPNBus.
//
// The returned IPNBusWatcher's Close method must be called when done to release
// resources.
//
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
nil)
if err != nil {
return nil, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
res.Body.Close()
return nil, errors.New(res.Status)
}
dec := json.NewDecoder(res.Body)
return &IPNBusWatcher{
ctx: ctx,
httpRes: res,
dec: dec,
}, nil
}
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
// It's returned by LocalClient.WatchIPNBus.
//
// It must be closed when done.
type IPNBusWatcher struct {
ctx context.Context // from original WatchIPNBus call
httpRes *http.Response
dec *json.Decoder
mu sync.Mutex
closed bool
}
// Close stops the watcher and releases its resources.
func (w *IPNBusWatcher) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return nil
}
w.closed = true
return w.httpRes.Body.Close()
}
// Next returns the next ipn.Notify from the stream.
// If the context from LocalClient.WatchIPNBus is done, that error is returned.
func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
var n ipn.Notify
if err := w.dec.Decode(&n); err != nil {
if cerr := w.ctx.Err(); cerr != nil {
err = cerr
}
return ipn.Notify{}, err
}
return n, nil
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.19
package tailscale
import "testing"
func TestGetServeConfigFromJSON(t *testing.T) {
sc, err := getServeConfigFromJSON([]byte("null"))
if sc != nil {
t.Errorf("want nil for null")
}
if err != nil {
t.Errorf("reading null: %v", err)
}
sc, err = getServeConfigFromJSON([]byte(`{"TCP":{}}`))
if err != nil {
t.Errorf("reading object: %v", err)
} else if sc == nil {
t.Errorf("want non-nil for object")
} else if sc.TCP == nil {
t.Errorf("want non-nil TCP for object")
}
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !go1.19
// +build !go1.19
package tailscale

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package tailscale

View File

@@ -3,9 +3,8 @@
// license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
// Package tailscale contains Go clients for the Tailscale Local API and
// Package tailscale contains Go clients for the Tailscale LocalAPI and
// Tailscale control plane API.
//
// Warning: this package is in development and makes no API compatibility
@@ -17,7 +16,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
)
@@ -116,7 +114,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.httpClient().Do(req)
}
// sendRequest add the authenication key to the request and sends it. It
// sendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
if !I_Acknowledge_This_API_Is_Unstable {
@@ -131,7 +129,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
// Read response. Limit the response to 10MB.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := ioutil.ReadAll(body)
b, err := io.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}

View File

@@ -80,7 +80,7 @@ func main() {
w("}")
}
cloneOutput := pkg.Name + "_clone.go"
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, codegen.CopyrightYear("."), it, buf); err != nil {
log.Fatal(err)
}
}

278
cmd/containerboot/kube.go Normal file
View File

@@ -0,0 +1,278 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"tailscale.com/tailcfg"
"tailscale.com/util/multierr"
)
// checkSecretPermissions checks the secret access permissions of the current
// pod. It returns an error if the basic permissions tailscale needs are
// missing, and reports whether the patch permission is additionally present.
//
// Errors encountered during the access checking process are logged, but ignored
// so that the pod tries to fail alive if the permissions exist and there's just
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
// should always be able to use SSARs to assess their own permissions, but since
// we didn't use to check permissions this way we'll be cautious in case some
// old version of k8s deviates from the current behavior.
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
var errs []error
for _, verb := range []string{"get", "update"} {
ok, err := checkPermission(ctx, verb, secretName)
if err != nil {
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
} else if !ok {
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
}
}
if len(errs) > 0 {
return false, multierr.New(errs...)
}
ok, err := checkPermission(ctx, "patch", secretName)
if err != nil {
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
return false, nil
}
return ok, nil
}
// checkPermission reports whether the current pod has permission to use the
// given verb (e.g. get, update, patch) on secretName.
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
sar := map[string]any{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SelfSubjectAccessReview",
"spec": map[string]any{
"resourceAttributes": map[string]any{
"namespace": kubeNamespace,
"verb": verb,
"resource": "secrets",
"name": secretName,
},
},
}
bs, err := json.Marshal(sar)
if err != nil {
return false, err
}
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
if err != nil {
return false, err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
return false, err
}
defer resp.Body.Close()
bs, err = io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var res struct {
Status struct {
Allowed bool `json:"allowed"`
} `json:"status"`
}
if err := json.Unmarshal(bs, &res); err != nil {
return false, err
}
return res.Status.Allowed, nil
}
// findKeyInKubeSecret inspects the kube secret secretName for a data
// field called "authkey", and returns its value if present.
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
if err != nil {
return "", err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// Kube secret doesn't exist yet, can't have an authkey.
return "", nil
}
return "", err
}
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// We use a map[string]any here rather than import corev1.Secret,
// because we only do very limited things to the secret, and
// importing corev1 adds 12MiB to the compiled binary.
var s map[string]any
if err := json.Unmarshal(bs, &s); err != nil {
return "", err
}
if d, ok := s["data"].(map[string]any); ok {
if v, ok := d["authkey"].(string); ok {
bs, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return "", err
}
return string(bs), nil
}
}
return "", nil
}
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
// secret secretName.
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
// First check if the secret exists at all. Even if running on
// kubernetes, we do not necessarily store state in a k8s secret.
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
if err != nil {
return err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
// Assume the secret doesn't exist, or we don't have
// permission to access it.
return nil
}
return err
}
m := map[string]map[string]string{
"stringData": {
"device_id": string(deviceID),
"device_fqdn": fqdn,
},
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
if _, err := doKubeRequest(ctx, req); err != nil {
return err
}
return nil
}
// deleteAuthKey deletes the 'authkey' field of the given kube
// secret. No-op if there is no authkey in the secret.
func deleteAuthKey(ctx context.Context, secretName string) error {
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
m := []struct {
Op string `json:"op"`
Path string `json:"path"`
}{
{
Op: "remove",
Path: "/data/authkey",
},
}
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json-patch+json")
if resp, err := doKubeRequest(ctx, req); err != nil {
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
// This is kubernetes-ese for "the field you asked to
// delete already doesn't exist", aka no-op.
return nil
}
return err
}
return nil
}
var (
kubeHost string
kubeNamespace string
kubeToken string
kubeHTTP *http.Transport
)
func initKube(root string) {
// If running in Kubernetes, set things up so that doKubeRequest
// can talk successfully to the kube apiserver.
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
return
}
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
if err != nil {
log.Fatalf("Error reading kube namespace: %v", err)
}
kubeNamespace = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
if err != nil {
log.Fatalf("Error reading kube token: %v", err)
}
kubeToken = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
if err != nil {
log.Fatalf("Error reading kube CA cert: %v", err)
}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(bs)
kubeHTTP = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: cp,
},
IdleConnTimeout: time.Second,
}
}
// doKubeRequest sends r to the kube apiserver.
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
if kubeHTTP == nil {
panic("not in kubernetes")
}
r.URL.Scheme = "https"
r.URL.Host = kubeHost
r.Header.Set("Authorization", "Bearer "+kubeToken)
r.Header.Set("Accept", "application/json")
resp, err := kubeHTTP.RoundTrip(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
}
return resp, nil
}

569
cmd/containerboot/main.go Normal file
View File

@@ -0,0 +1,569 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
// The containerboot binary is a wrapper for starting tailscaled in a container.
// It handles reading the desired mode of operation out of environment
// variables, bringing up and authenticating Tailscale, and any other
// kubernetes-specific side jobs.
//
// As with most container things, configuration is passed through environment
// variables. All configuration is optional.
//
// - TS_AUTHKEY: the authkey to use for login.
// - TS_ROUTES: subnet routes to advertise.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination.
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
// - TS_USERSPACE: run with userspace networking (the default)
// instead of kernel networking.
// - TS_STATE_DIR: the directory in which to store tailscaled
// state. The data should persist across container
// restarts.
// - TS_ACCEPT_DNS: whether to use the tailnet's DNS configuration.
// - TS_KUBE_SECRET: the name of the Kubernetes secret in which to
// store tailscaled state.
// - TS_SOCKS5_SERVER: the address on which to listen for SOCKS5
// proxying into the tailnet.
// - TS_OUTBOUND_HTTP_PROXY_LISTEN: the address on which to listen
// for HTTP proxying into the tailnet.
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
// be created.
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
// logged in. If false (the default, for backwards
// compatibility), forcibly log in every time the
// container starts.
//
// When running on Kubernetes, containerboot defaults to storing state in the
// "tailscale" kube secret. To store state on local disk instead, set
// TS_KUBE_SECRET="" and TS_STATE_DIR=/path/to/storage/dir. The state dir should
// be persistent storage.
//
// Additionally, if TS_AUTHKEY is not set and the TS_KUBE_SECRET contains an
// "authkey" field, that key is used as the tailscale authkey.
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"log"
"net/netip"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/util/deephash"
)
func main() {
log.SetPrefix("boot: ")
tailscale.I_Acknowledge_This_API_Is_Unstable = true
cfg := &settings{
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
Routes: defaultEnv("TS_ROUTES", ""),
ProxyTo: defaultEnv("TS_DEST_IP", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
UserspaceMode: defaultBool("TS_USERSPACE", true),
StateDir: defaultEnv("TS_STATE_DIR", ""),
AcceptDNS: defaultBool("TS_ACCEPT_DNS", false),
KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
}
if cfg.ProxyTo != "" && cfg.UserspaceMode {
log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE")
}
if !cfg.UserspaceMode {
if err := ensureTunFile(cfg.Root); err != nil {
log.Fatalf("Unable to create tuntap device file: %v", err)
}
if cfg.ProxyTo != "" || cfg.Routes != "" {
if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.Routes); err != nil {
log.Printf("Failed to enable IP forwarding: %v", err)
log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.")
if cfg.InKubernetes {
log.Fatalf("You can either set the sysctls as a privileged initContainer, or run the tailscale container with privileged=true.")
} else {
log.Fatalf("You can fix this by running the container with privileged=true, or the equivalent in your container runtime that permits access to sysctls.")
}
}
}
}
if cfg.InKubernetes {
initKube(cfg.Root)
}
// Context is used for all setup stuff until we're in steady
// state, so that if something is hanging we eventually time out
// and crashloop the container.
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if cfg.InKubernetes && cfg.KubeSecret != "" {
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
}
cfg.KubernetesCanPatch = canPatch
if cfg.AuthKey == "" {
key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Getting authkey from kube secret: %v", err)
}
if key != "" {
// This behavior of pulling authkeys from kube secrets was added
// at the same time as the patch permission, so we can enforce
// that we must be able to patch out the authkey after
// authenticating if you want to use this feature. This avoids
// us having to deal with the case where we might leave behind
// an unnecessary reusable authkey in a secret, like a rake in
// the grass.
if !cfg.KubernetesCanPatch {
log.Fatalf("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the secret to manage the authkey.")
}
log.Print("Using authkey found in kube secret")
cfg.AuthKey = key
} else {
log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
}
}
}
client, daemonPid, err := startTailscaled(ctx, cfg)
if err != nil {
log.Fatalf("failed to bring up tailscale: %v", err)
}
w, err := client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState)
if err != nil {
log.Fatalf("failed to watch tailscaled for updates: %v", err)
}
// Because we're still shelling out to `tailscale up` to get access to its
// flag parser, we have to stop watching the IPN bus so that we can block on
// the subcommand without stalling anything. Then once it's done, we resume
// watching the bus.
//
// Depending on the requested mode of operation, this auth step happens at
// different points in containerboot's lifecycle, hence the helper function.
didLogin := false
authTailscale := func() error {
if didLogin {
return nil
}
didLogin = true
w.Close()
if err := tailscaleUp(ctx, cfg); err != nil {
return fmt.Errorf("failed to auth tailscale: %v", err)
}
w, err = client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
if err != nil {
return fmt.Errorf("rewatching tailscaled for updates after auth: %v", err)
}
return nil
}
if !cfg.AuthOnce {
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
}
authLoop:
for {
n, err := w.Next()
if err != nil {
log.Fatalf("failed to read from tailscaled: %v", err)
}
if n.State != nil {
switch *n.State {
case ipn.NeedsLogin:
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
case ipn.NeedsMachineAuth:
log.Printf("machine authorization required, please visit the admin panel")
case ipn.Running:
// Technically, all we want is to keep monitoring the bus for
// netmap updates. However, in order to make the container crash
// if tailscale doesn't initially come up, the watch has a
// startup deadline on it. So, we have to break out of this
// watch loop, cancel the watch, and watch again with no
// deadline to continue monitoring for changes.
break authLoop
default:
log.Printf("tailscaled in state %q, waiting", *n.State)
}
}
}
w.Close()
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
// We were told to only auth once, so any secret-bound
// authkey is no longer needed. We don't strictly need to
// wipe it, but it's good hygiene.
log.Printf("Deleting authkey from kube secret")
if err := deleteAuthKey(ctx, cfg.KubeSecret); err != nil {
log.Fatalf("deleting authkey from kube secret: %v", err)
}
}
w, err = client.WatchIPNBus(context.Background(), ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
if err != nil {
log.Fatalf("rewatching tailscaled for updates after auth: %v", err)
}
var (
wantProxy = cfg.ProxyTo != ""
wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
startupTasksDone = false
currentIPs deephash.Sum // tailscale IPs assigned to device
currentDeviceInfo deephash.Sum // device ID and fqdn
)
for {
n, err := w.Next()
if err != nil {
log.Fatalf("failed to read from tailscaled: %v", err)
}
if n.State != nil && *n.State != ipn.Running {
// Something's gone wrong and we've left the authenticated state.
// Our container image never recovered gracefully from this, and the
// control flow required to make it work now is hard. So, just crash
// the container and rely on the container runtime to restart us,
// whereupon we'll go through initial auth again.
log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State)
}
if n.NetMap != nil {
if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(&currentIPs, &n.NetMap.Addresses) {
if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil {
log.Fatalf("installing proxy rules: %v", err)
}
}
deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name}
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) {
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil {
log.Fatalf("storing device ID in kube secret: %v", err)
}
}
}
if !startupTasksDone {
if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) {
// This log message is used in tests to detect when all
// post-auth configuration is done.
log.Println("Startup complete, waiting for shutdown signal")
startupTasksDone = true
// Reap all processes, since we are PID1 and need to collect zombies. We can
// only start doing this once we've stopped shelling out to things
// `tailscale up`, otherwise this goroutine can reap the CLI subprocesses
// and wedge bringup.
go func() {
for {
var status unix.WaitStatus
pid, err := unix.Wait4(-1, &status, 0, nil)
if errors.Is(err, unix.EINTR) {
continue
}
if err != nil {
log.Fatalf("Waiting for exited processes: %v", err)
}
if pid == daemonPid {
log.Printf("Tailscaled exited")
os.Exit(0)
}
}
}()
}
}
}
}
func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, int, error) {
args := tailscaledArgs(cfg)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT)
// tailscaled runs without context, since it needs to persist
// beyond the startup timeout in ctx.
cmd := exec.Command("tailscaled", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
log.Printf("Starting tailscaled")
if err := cmd.Start(); err != nil {
return nil, 0, fmt.Errorf("starting tailscaled failed: %v", err)
}
go func() {
<-sigCh
log.Printf("Received SIGTERM from container runtime, shutting down tailscaled")
cmd.Process.Signal(unix.SIGTERM)
}()
// Wait for the socket file to appear, otherwise API ops will racily fail.
log.Printf("Waiting for tailscaled socket")
for {
if ctx.Err() != nil {
log.Fatalf("Timed out waiting for tailscaled socket")
}
_, err := os.Stat(cfg.Socket)
if errors.Is(err, fs.ErrNotExist) {
time.Sleep(100 * time.Millisecond)
continue
} else if err != nil {
log.Fatalf("Waiting for tailscaled socket: %v", err)
}
break
}
tsClient := &tailscale.LocalClient{
Socket: cfg.Socket,
UseSocketOnly: true,
}
return tsClient, cmd.Process.Pid, nil
}
// tailscaledArgs uses cfg to construct the argv for tailscaled.
func tailscaledArgs(cfg *settings) []string {
args := []string{"--socket=" + cfg.Socket}
switch {
case cfg.InKubernetes && cfg.KubeSecret != "":
args = append(args, "--state=kube:"+cfg.KubeSecret, "--statedir=/tmp")
case cfg.StateDir != "":
args = append(args, "--statedir="+cfg.StateDir)
default:
args = append(args, "--state=mem:", "--statedir=/tmp")
}
if cfg.UserspaceMode {
args = append(args, "--tun=userspace-networking")
} else if err := ensureTunFile(cfg.Root); err != nil {
log.Fatalf("ensuring that /dev/net/tun exists: %v", err)
}
if cfg.SOCKSProxyAddr != "" {
args = append(args, "--socks5-server="+cfg.SOCKSProxyAddr)
}
if cfg.HTTPProxyAddr != "" {
args = append(args, "--outbound-http-proxy-listen="+cfg.HTTPProxyAddr)
}
if cfg.DaemonExtraArgs != "" {
args = append(args, strings.Fields(cfg.DaemonExtraArgs)...)
}
return args
}
// tailscaleUp uses cfg to run 'tailscale up'.
func tailscaleUp(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "up"}
if cfg.AcceptDNS {
args = append(args, "--accept-dns=true")
} else {
args = append(args, "--accept-dns=false")
}
if cfg.AuthKey != "" {
args = append(args, "--authkey="+cfg.AuthKey)
}
if cfg.Routes != "" {
args = append(args, "--advertise-routes="+cfg.Routes)
}
if cfg.ExtraArgs != "" {
args = append(args, strings.Fields(cfg.ExtraArgs)...)
}
log.Printf("Running 'tailscale up'")
cmd := exec.CommandContext(ctx, "tailscale", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("tailscale up failed: %v", err)
}
return nil
}
// ensureTunFile checks that /dev/net/tun exists, creating it if
// missing.
func ensureTunFile(root string) error {
// Verify that /dev/net/tun exists, in some container envs it
// needs to be mknod-ed.
if _, err := os.Stat(filepath.Join(root, "dev/net")); errors.Is(err, fs.ErrNotExist) {
if err := os.MkdirAll(filepath.Join(root, "dev/net"), 0755); err != nil {
return err
}
}
if _, err := os.Stat(filepath.Join(root, "dev/net/tun")); errors.Is(err, fs.ErrNotExist) {
dev := unix.Mkdev(10, 200) // tuntap major and minor
if err := unix.Mknod(filepath.Join(root, "dev/net/tun"), 0600|unix.S_IFCHR, int(dev)); err != nil {
return err
}
}
return nil
}
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
func ensureIPForwarding(root, proxyTo, routes string) error {
var (
v4Forwarding, v6Forwarding bool
)
if proxyTo != "" {
proxyIP, err := netip.ParseAddr(proxyTo)
if err != nil {
return fmt.Errorf("invalid proxy destination IP: %v", err)
}
if proxyIP.Is4() {
v4Forwarding = true
} else {
v6Forwarding = true
}
}
if routes != "" {
for _, route := range strings.Split(routes, ",") {
cidr, err := netip.ParsePrefix(route)
if err != nil {
return fmt.Errorf("invalid subnet route: %v", err)
}
if cidr.Addr().Is4() {
v4Forwarding = true
} else {
v6Forwarding = true
}
}
}
var paths []string
if v4Forwarding {
paths = append(paths, filepath.Join(root, "proc/sys/net/ipv4/ip_forward"))
}
if v6Forwarding {
paths = append(paths, filepath.Join(root, "proc/sys/net/ipv6/conf/all/forwarding"))
}
// In some common configurations (e.g. default docker,
// kubernetes), the container environment denies write access to
// most sysctls, including IP forwarding controls. Check the
// sysctl values before trying to change them, so that we
// gracefully do nothing if the container's already been set up
// properly by e.g. a k8s initContainer.
for _, path := range paths {
bs, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading %q: %w", path, err)
}
if v := strings.TrimSpace(string(bs)); v != "1" {
if err := os.WriteFile(path, []byte("1"), 0644); err != nil {
return fmt.Errorf("enabling %q: %w", path, err)
}
}
}
return nil
}
func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
dst, err := netip.ParseAddr(dstStr)
if err != nil {
return err
}
argv0 := "iptables"
if dst.Is6() {
argv0 = "ip6tables"
}
var local string
for _, pfx := range tsIPs {
if !pfx.IsSingleIP() {
continue
}
if pfx.Addr().Is4() != dst.Is4() {
continue
}
local = pfx.Addr().String()
break
}
if local == "" {
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
}
// Technically, if the control server ever changes the IPs assigned to this
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
// for now we'll live with it.
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
}
return nil
}
// settings is all the configuration for containerboot.
type settings struct {
AuthKey string
Routes string
ProxyTo string
DaemonExtraArgs string
ExtraArgs string
InKubernetes bool
UserspaceMode bool
StateDir string
AcceptDNS bool
KubeSecret string
SOCKSProxyAddr string
HTTPProxyAddr string
Socket string
AuthOnce bool
Root string
KubernetesCanPatch bool
}
// defaultEnv returns the value of the given envvar name, or defVal if
// unset.
func defaultEnv(name, defVal string) string {
if v, ok := os.LookupEnv(name); ok {
return v
}
return defVal
}
func defaultEnvs(names []string, defVal string) string {
for _, name := range names {
if v, ok := os.LookupEnv(name); ok {
return v
}
}
return defVal
}
// defaultBool returns the boolean value of the given envvar name, or
// defVal if unset or not a bool.
func defaultBool(name string, defVal bool) bool {
v := os.Getenv(name)
ret, err := strconv.ParseBool(v)
if err != nil {
return defVal
}
return ret
}

View File

@@ -0,0 +1,985 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
package main
import (
"bytes"
_ "embed"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/sys/unix"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
)
func TestContainerBoot(t *testing.T) {
d := t.TempDir()
lapi := localAPI{FSRoot: d}
if err := lapi.Start(); err != nil {
t.Fatal(err)
}
defer lapi.Close()
kube := kubeServer{FSRoot: d}
if err := kube.Start(); err != nil {
t.Fatal(err)
}
defer kube.Close()
dirs := []string{
"var/lib",
"usr/bin",
"tmp",
"dev/net",
"proc/sys/net/ipv4",
"proc/sys/net/ipv6/conf/all",
}
for _, path := range dirs {
if err := os.MkdirAll(filepath.Join(d, path), 0700); err != nil {
t.Fatal(err)
}
}
files := map[string][]byte{
"usr/bin/tailscaled": fakeTailscaled,
"usr/bin/tailscale": fakeTailscale,
"usr/bin/iptables": fakeTailscale,
"usr/bin/ip6tables": fakeTailscale,
"dev/net/tun": []byte(""),
"proc/sys/net/ipv4/ip_forward": []byte("0"),
"proc/sys/net/ipv6/conf/all/forwarding": []byte("0"),
}
resetFiles := func() {
for path, content := range files {
// Making everything executable is a little weird, but the
// stuff that doesn't need to be executable doesn't care if we
// do make it executable.
if err := os.WriteFile(filepath.Join(d, path), content, 0700); err != nil {
t.Fatal(err)
}
}
}
resetFiles()
boot := filepath.Join(d, "containerboot")
if err := exec.Command("go", "build", "-o", boot, "tailscale.com/cmd/containerboot").Run(); err != nil {
t.Fatalf("Building containerboot: %v", err)
}
argFile := filepath.Join(d, "args")
runningSockPath := filepath.Join(d, "tmp/tailscaled.sock")
type phase struct {
// If non-nil, send this IPN bus notification (and remember it as the
// initial update for any future new watchers, then wait for all the
// Waits below to be true before proceeding to the next phase.
Notify *ipn.Notify
// WantCmds is the commands that containerboot should run in this phase.
WantCmds []string
// WantKubeSecret is the secret keys/values that should exist in the
// kube secret.
WantKubeSecret map[string]string
// WantFiles files that should exist in the container and their
// contents.
WantFiles map[string]string
}
runningNotify := &ipn.Notify{
State: ptr.To(ipn.Running),
NetMap: &netmap.NetworkMap{
SelfNode: &tailcfg.Node{
StableID: tailcfg.StableNodeID("myID"),
Name: "test-node.test.ts.net",
},
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
},
}
tests := []struct {
Name string
Env map[string]string
KubeSecret map[string]string
KubeDenyPatch bool
Phases []phase
}{
{
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
Name: "no_args",
Env: nil,
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
},
},
{
Notify: runningNotify,
},
},
},
{
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
},
},
},
{
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey-old-flag",
Env: map[string]string{
"TS_AUTH_KEY": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
},
},
},
{
Name: "authkey_disk_state",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
},
},
},
{
Name: "routes",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
},
},
},
{
Name: "routes_kernel_ipv4",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
"TS_USERSPACE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "1",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
},
},
},
{
Name: "routes_kernel_ipv6",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1::/64",
"TS_USERSPACE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "1",
},
},
},
},
{
Name: "routes_kernel_all_families",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1.2.3.0/24",
"TS_USERSPACE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "1",
"proc/sys/net/ipv6/conf/all/forwarding": "1",
},
},
},
},
{
Name: "proxy",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_DEST_IP": "1.2.3.4",
"TS_USERSPACE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
},
},
},
},
{
Name: "authkey_once",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_AUTH_ONCE": "true",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
},
},
{
Notify: &ipn.Notify{
State: ptr.To(ipn.NeedsLogin),
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
},
},
},
{
Name: "kube_storage",
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
},
},
{
Notify: runningNotify,
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net",
"device_id": "myID",
},
},
},
},
{
Name: "kube_disk_storage",
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
// Explicitly set to an empty value, to override the default of "tailscale".
"TS_KUBE_SECRET": "",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
"TS_AUTHKEY": "tskey-key",
},
KubeSecret: map[string]string{},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{},
},
{
Notify: runningNotify,
WantKubeSecret: map[string]string{},
},
},
},
{
Name: "kube_storage_no_patch",
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTHKEY": "tskey-key",
},
KubeSecret: map[string]string{},
KubeDenyPatch: true,
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{},
},
{
Notify: runningNotify,
WantKubeSecret: map[string]string{},
},
},
},
{
// Same as previous, but deletes the authkey from the kube secret.
Name: "kube_storage_auth_once",
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTH_ONCE": "true",
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
},
},
{
Notify: &ipn.Notify{
State: ptr.To(ipn.NeedsLogin),
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
},
},
{
Notify: runningNotify,
WantKubeSecret: map[string]string{
"device_fqdn": "test-node.test.ts.net",
"device_id": "myID",
},
},
},
},
{
Name: "kube_storage_updates",
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
},
},
{
Notify: runningNotify,
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net",
"device_id": "myID",
},
},
{
Notify: &ipn.Notify{
State: ptr.To(ipn.Running),
NetMap: &netmap.NetworkMap{
SelfNode: &tailcfg.Node{
StableID: tailcfg.StableNodeID("newID"),
Name: "new-name.test.ts.net",
},
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
},
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
"device_fqdn": "new-name.test.ts.net",
"device_id": "newID",
},
},
},
},
{
Name: "proxies",
Env: map[string]string{
"TS_SOCKS5_SERVER": "localhost:1080",
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
},
},
{
Notify: runningNotify,
},
},
},
{
Name: "dns",
Env: map[string]string{
"TS_ACCEPT_DNS": "true",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
},
},
{
Notify: runningNotify,
},
},
},
{
Name: "extra_args",
Env: map[string]string{
"TS_EXTRA_ARGS": "--widget=rotated",
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
},
}, {
Notify: runningNotify,
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
lapi.Reset()
kube.Reset()
os.Remove(argFile)
os.Remove(runningSockPath)
resetFiles()
for k, v := range test.KubeSecret {
kube.SetSecret(k, v)
}
kube.SetPatching(!test.KubeDenyPatch)
cmd := exec.Command(boot)
cmd.Env = []string{
fmt.Sprintf("PATH=%s/usr/bin:%s", d, os.Getenv("PATH")),
fmt.Sprintf("TS_TEST_RECORD_ARGS=%s", argFile),
fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
}
for k, v := range test.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
cbOut := &lockingBuffer{}
defer func() {
if t.Failed() {
t.Logf("containerboot output:\n%s", cbOut.String())
}
}()
cmd.Stderr = cbOut
if err := cmd.Start(); err != nil {
t.Fatalf("starting containerboot: %v", err)
}
defer func() {
cmd.Process.Signal(unix.SIGTERM)
cmd.Process.Wait()
}()
var wantCmds []string
for _, p := range test.Phases {
lapi.Notify(p.Notify)
wantCmds = append(wantCmds, p.WantCmds...)
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
err := tstest.WaitFor(2*time.Second, func() error {
if p.WantKubeSecret != nil {
got := kube.Secret()
if diff := cmp.Diff(got, p.WantKubeSecret); diff != "" {
return fmt.Errorf("unexpected kube secret data (-got+want):\n%s", diff)
}
} else {
got := kube.Secret()
if len(got) > 0 {
return fmt.Errorf("kube secret unexpectedly not empty, got %#v", got)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
err = tstest.WaitFor(2*time.Second, func() error {
for path, want := range p.WantFiles {
gotBs, err := os.ReadFile(filepath.Join(d, path))
if err != nil {
return fmt.Errorf("reading wanted file %q: %v", path, err)
}
if got := strings.TrimSpace(string(gotBs)); got != want {
return fmt.Errorf("wrong file contents for %q, got %q want %q", path, got, want)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
waitLogLine(t, 2*time.Second, cbOut, "Startup complete, waiting for shutdown signal")
})
}
}
type lockingBuffer struct {
sync.Mutex
b bytes.Buffer
}
func (b *lockingBuffer) Write(bs []byte) (int, error) {
b.Lock()
defer b.Unlock()
return b.b.Write(bs)
}
func (b *lockingBuffer) String() string {
b.Lock()
defer b.Unlock()
return b.b.String()
}
// waitLogLine looks for want in the contents of b.
//
// Only lines starting with 'boot: ' (the output of containerboot
// itself) are considered, and the logged timestamp is ignored.
//
// waitLogLine fails the entire test if path doesn't contain want
// before the timeout.
func waitLogLine(t *testing.T, timeout time.Duration, b *lockingBuffer, want string) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
for _, line := range strings.Split(b.String(), "\n") {
if !strings.HasPrefix(line, "boot: ") {
continue
}
if strings.HasSuffix(line, " "+want) {
return
}
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("timed out waiting for wanted output line %q. Output:\n%s", want, b.String())
}
// waitArgs waits until the contents of path matches wantArgs, a set
// of command lines recorded by test_tailscale.sh and
// test_tailscaled.sh.
//
// All occurrences of removeStr are removed from the file prior to
// comparison. This is used to remove the varying temporary root
// directory name from recorded commandlines, so that wantArgs can be
// a constant value.
//
// waitArgs fails the entire test if path doesn't contain wantArgs
// before the timeout.
func waitArgs(t *testing.T, timeout time.Duration, removeStr, path, wantArgs string) {
t.Helper()
wantArgs = strings.TrimSpace(wantArgs)
deadline := time.Now().Add(timeout)
var got string
for time.Now().Before(deadline) {
bs, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
// Don't bother logging that the file doesn't exist, it
// should start existing soon.
goto loop
} else if err != nil {
t.Logf("reading %q: %v", path, err)
goto loop
}
got = strings.TrimSpace(string(bs))
got = strings.ReplaceAll(got, removeStr, "")
if got == wantArgs {
return
}
loop:
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("waiting for args file %q to have expected output, got:\n%s\n\nWant: %s", path, got, wantArgs)
}
//go:embed test_tailscaled.sh
var fakeTailscaled []byte
//go:embed test_tailscale.sh
var fakeTailscale []byte
// localAPI is a minimal fake tailscaled LocalAPI server that presents
// just enough functionality for containerboot to function
// correctly. In practice this means it only supports querying
// tailscaled status, and panics on all other uses to make it very
// obvious that something unexpected happened.
type localAPI struct {
FSRoot string
Path string // populated by Start
srv *http.Server
sync.Mutex
cond *sync.Cond
notify *ipn.Notify
}
func (l *localAPI) Start() error {
path := filepath.Join(l.FSRoot, "tmp/tailscaled.sock.fake")
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
ln, err := net.Listen("unix", path)
if err != nil {
return err
}
l.srv = &http.Server{
Handler: l,
}
l.Path = path
l.cond = sync.NewCond(&l.Mutex)
go l.srv.Serve(ln)
return nil
}
func (l *localAPI) Close() {
l.srv.Close()
}
func (l *localAPI) Reset() {
l.Lock()
defer l.Unlock()
l.notify = nil
l.cond.Broadcast()
}
func (l *localAPI) Notify(n *ipn.Notify) {
if n == nil {
return
}
l.Lock()
defer l.Unlock()
l.notify = n
l.cond.Broadcast()
}
func (l *localAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
panic(fmt.Sprintf("unsupported method %q", r.Method))
}
if r.URL.Path != "/localapi/v0/watch-ipn-bus" {
panic(fmt.Sprintf("unsupported path %q", r.URL.Path))
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
enc := json.NewEncoder(w)
l.Lock()
defer l.Unlock()
for {
if l.notify != nil {
if err := enc.Encode(l.notify); err != nil {
// Usually broken pipe as the test client disconnects.
return
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
l.cond.Wait()
}
}
// kubeServer is a minimal fake Kubernetes server that presents just
// enough functionality for containerboot to function correctly. In
// practice this means it only supports reading and modifying a single
// kube secret, and panics on all other uses to make it very obvious
// that something unexpected happened.
type kubeServer struct {
FSRoot string
Host, Port string // populated by Start
srv *httptest.Server
sync.Mutex
secret map[string]string
canPatch bool
}
func (k *kubeServer) Secret() map[string]string {
k.Lock()
defer k.Unlock()
ret := map[string]string{}
for k, v := range k.secret {
ret[k] = v
}
return ret
}
func (k *kubeServer) SetSecret(key, val string) {
k.Lock()
defer k.Unlock()
k.secret[key] = val
}
func (k *kubeServer) SetPatching(canPatch bool) {
k.Lock()
defer k.Unlock()
k.canPatch = canPatch
}
func (k *kubeServer) Reset() {
k.Lock()
defer k.Unlock()
k.secret = map[string]string{}
}
func (k *kubeServer) Start() error {
root := filepath.Join(k.FSRoot, "var/run/secrets/kubernetes.io/serviceaccount")
if err := os.MkdirAll(root, 0700); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(root, "namespace"), []byte("default"), 0600); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(root, "token"), []byte("bearer_token"), 0600); err != nil {
return err
}
k.srv = httptest.NewTLSServer(k)
k.Host = k.srv.Listener.Addr().(*net.TCPAddr).IP.String()
k.Port = strconv.Itoa(k.srv.Listener.Addr().(*net.TCPAddr).Port)
var cert bytes.Buffer
if err := pem.Encode(&cert, &pem.Block{Type: "CERTIFICATE", Bytes: k.srv.Certificate().Raw}); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(root, "ca.crt"), cert.Bytes(), 0600); err != nil {
return err
}
return nil
}
func (k *kubeServer) Close() {
k.srv.Close()
}
func (k *kubeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer bearer_token" {
panic("client didn't provide bearer token in request")
}
switch r.URL.Path {
case "/api/v1/namespaces/default/secrets/tailscale":
k.serveSecret(w, r)
case "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews":
k.serveSSAR(w, r)
default:
panic(fmt.Sprintf("unhandled fake kube api path %q", r.URL.Path))
}
}
func (k *kubeServer) serveSSAR(w http.ResponseWriter, r *http.Request) {
var req struct {
Spec struct {
ResourceAttributes struct {
Verb string `json:"verb"`
} `json:"resourceAttributes"`
} `json:"spec"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
panic(fmt.Sprintf("decoding SSAR request: %v", err))
}
ok := true
if req.Spec.ResourceAttributes.Verb == "patch" {
k.Lock()
defer k.Unlock()
ok = k.canPatch
}
// Just say yes to all SARs, we don't enforce RBAC.
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":{"allowed":%v}}`, ok)
}
func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
bs, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusInternalServerError)
return
}
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
ret := map[string]map[string]string{
"data": {},
}
k.Lock()
defer k.Unlock()
for k, v := range k.secret {
v := base64.StdEncoding.EncodeToString([]byte(v))
if err != nil {
panic("encode failed")
}
ret["data"][k] = v
}
if err := json.NewEncoder(w).Encode(ret); err != nil {
panic("encode failed")
}
case "PATCH":
k.Lock()
defer k.Unlock()
if !k.canPatch {
panic("containerboot tried to patch despite not being allowed")
}
switch r.Header.Get("Content-Type") {
case "application/json-patch+json":
req := []struct {
Op string `json:"op"`
Path string `json:"path"`
}{}
if err := json.Unmarshal(bs, &req); err != nil {
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
}
for _, op := range req {
if op.Op != "remove" {
panic(fmt.Sprintf("unsupported json-patch op %q", op.Op))
}
if !strings.HasPrefix(op.Path, "/data/") {
panic(fmt.Sprintf("unsupported json-patch path %q", op.Path))
}
delete(k.secret, strings.TrimPrefix(op.Path, "/data/"))
}
case "application/strategic-merge-patch+json":
req := struct {
Data map[string]string `json:"stringData"`
}{}
if err := json.Unmarshal(bs, &req); err != nil {
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
}
for key, val := range req.Data {
k.secret[key] = val
}
default:
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
}
default:
panic(fmt.Sprintf("unhandled HTTP method %q", r.Method))
}
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
#
# This is a fake tailscale CLI (and also iptables and ip6tables) that
# records its arguments and exits successfully.
#
# It is used by main_test.go to test the behavior of containerboot.
echo $0 $@ >>$TS_TEST_RECORD_ARGS

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
#
# This is a fake tailscale CLI that records its arguments, symlinks a
# fake LocalAPI socket into place, and does nothing until terminated.
#
# It is used by main_test.go to test the behavior of containerboot.
set -eu
echo $0 $@ >>$TS_TEST_RECORD_ARGS
socket=""
while [[ $# -gt 0 ]]; do
case $1 in
--socket=*)
socket="${1#--socket=}"
shift
;;
--socket)
shift
socket="$1"
shift
;;
*)
shift
;;
esac
done
if [[ -z "$socket" ]]; then
echo "didn't find socket path in args"
exit 1
fi
ln -s "$TS_TEST_SOCKET" "$socket"
while true; do sleep 1; done

View File

@@ -17,16 +17,31 @@ import (
"tailscale.com/syncs"
)
var dnsCache syncs.AtomicValue[[]byte]
const refreshTimeout = time.Minute
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
type dnsEntryMap map[string][]net.IP
var (
dnsCache syncs.AtomicValue[dnsEntryMap]
dnsCacheBytes syncs.AtomicValue[[]byte] // of JSON
unpublishedDNSCache syncs.AtomicValue[dnsEntryMap]
)
var (
bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
publishedDNSHits = expvar.NewInt("counter_bootstrap_dns_published_hits")
publishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_published_misses")
unpublishedDNSHits = expvar.NewInt("counter_bootstrap_dns_unpublished_hits")
unpublishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_misses")
)
func refreshBootstrapDNSLoop() {
if *bootstrapDNS == "" {
if *bootstrapDNS == "" && *unpublishedDNS == "" {
return
}
for {
refreshBootstrapDNS()
refreshUnpublishedDNS()
time.Sleep(10 * time.Minute)
}
}
@@ -35,10 +50,34 @@ func refreshBootstrapDNS() {
if *bootstrapDNS == "" {
return
}
dnsEntries := make(map[string][]net.IP)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
defer cancel()
names := strings.Split(*bootstrapDNS, ",")
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
j, err := json.MarshalIndent(dnsEntries, "", "\t")
if err != nil {
// leave the old values in place
return
}
dnsCache.Store(dnsEntries)
dnsCacheBytes.Store(j)
}
func refreshUnpublishedDNS() {
if *unpublishedDNS == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
defer cancel()
dnsEntries := resolveList(ctx, strings.Split(*unpublishedDNS, ","))
unpublishedDNSCache.Store(dnsEntries)
}
func resolveList(ctx context.Context, names []string) dnsEntryMap {
dnsEntries := make(dnsEntryMap)
var r net.Resolver
for _, name := range names {
addrs, err := r.LookupIP(ctx, "ip", name)
@@ -48,21 +87,47 @@ func refreshBootstrapDNS() {
}
dnsEntries[name] = addrs
}
j, err := json.MarshalIndent(dnsEntries, "", "\t")
if err != nil {
// leave the old values in place
return
}
dnsCache.Store(j)
return dnsEntries
}
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
bootstrapDNSRequests.Add(1)
w.Header().Set("Content-Type", "application/json")
j := dnsCache.Load()
// Bootstrap DNS requests occur cross-regions,
// and are randomized per request,
// so keeping a connection open is pointlessly expensive.
// Bootstrap DNS requests occur cross-regions, and are randomized per
// request, so keeping a connection open is pointlessly expensive.
w.Header().Set("Connection", "close")
// Try answering a query from our hidden map first
if q := r.URL.Query().Get("q"); q != "" {
if ips, ok := unpublishedDNSCache.Load()[q]; ok && len(ips) > 0 {
unpublishedDNSHits.Add(1)
// Only return the specific query, not everything.
m := dnsEntryMap{q: ips}
j, err := json.MarshalIndent(m, "", "\t")
if err == nil {
w.Write(j)
return
}
}
// If we have a "q" query for a name in the published cache
// list, then track whether that's a hit/miss.
if m, ok := dnsCache.Load()[q]; ok {
if len(m) > 0 {
publishedDNSHits.Add(1)
} else {
publishedDNSMisses.Add(1)
}
} else {
// If it wasn't in either cache, treat this as a query
// for the unpublished cache, and thus a cache miss.
unpublishedDNSMisses.Add(1)
}
}
// Fall back to returning the public set of cached DNS names
j := dnsCacheBytes.Load()
w.Write(j)
}

View File

@@ -5,7 +5,12 @@
package main
import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
)
@@ -17,11 +22,12 @@ func BenchmarkHandleBootstrapDNS(b *testing.B) {
}()
refreshBootstrapDNS()
w := new(bitbucketResponseWriter)
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(b *testing.PB) {
for b.Next() {
handleBootstrapDNS(w, nil)
handleBootstrapDNS(w, req)
}
})
}
@@ -33,3 +39,116 @@ func (b *bitbucketResponseWriter) Header() http.Header { return make(http.Header
func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p), nil }
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}
func getBootstrapDNS(t *testing.T, q string) dnsEntryMap {
t.Helper()
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape(q), nil)
w := httptest.NewRecorder()
handleBootstrapDNS(w, req)
res := w.Result()
if res.StatusCode != 200 {
t.Fatalf("got status=%d; want %d", res.StatusCode, 200)
}
var ips dnsEntryMap
if err := json.NewDecoder(res.Body).Decode(&ips); err != nil {
t.Fatalf("error decoding response body: %v", err)
}
return ips
}
func TestUnpublishedDNS(t *testing.T) {
const published = "login.tailscale.com"
const unpublished = "log.tailscale.io"
prev1, prev2 := *bootstrapDNS, *unpublishedDNS
*bootstrapDNS = published
*unpublishedDNS = unpublished
t.Cleanup(func() {
*bootstrapDNS = prev1
*unpublishedDNS = prev2
})
refreshBootstrapDNS()
refreshUnpublishedDNS()
hasResponse := func(q string) bool {
_, found := getBootstrapDNS(t, q)[q]
return found
}
if !hasResponse(published) {
t.Errorf("expected response for: %s", published)
}
if !hasResponse(unpublished) {
t.Errorf("expected response for: %s", unpublished)
}
// Verify that querying for a random query or a real query does not
// leak our unpublished domain
m1 := getBootstrapDNS(t, published)
if _, found := m1[unpublished]; found {
t.Errorf("found unpublished domain %s: %+v", unpublished, m1)
}
m2 := getBootstrapDNS(t, "random.example.com")
if _, found := m2[unpublished]; found {
t.Errorf("found unpublished domain %s: %+v", unpublished, m2)
}
}
func resetMetrics() {
publishedDNSHits.Set(0)
publishedDNSMisses.Set(0)
unpublishedDNSHits.Set(0)
unpublishedDNSMisses.Set(0)
}
// Verify that we don't count an empty list in the unpublishedDNSCache as a
// cache hit in our metrics.
func TestUnpublishedDNSEmptyList(t *testing.T) {
pub := dnsEntryMap{
"tailscale.com": {net.IPv4(10, 10, 10, 10)},
}
dnsCache.Store(pub)
dnsCacheBytes.Store([]byte(`{"tailscale.com":["10.10.10.10"]}`))
unpublishedDNSCache.Store(dnsEntryMap{
"log.tailscale.io": {},
"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)},
})
t.Run("CacheMiss", func(t *testing.T) {
// One domain in map but empty, one not in map at all
for _, q := range []string{"log.tailscale.io", "login.tailscale.com"} {
resetMetrics()
ips := getBootstrapDNS(t, q)
// Expected our public map to be returned on a cache miss
if !reflect.DeepEqual(ips, pub) {
t.Errorf("got ips=%+v; want %+v", ips, pub)
}
if v := unpublishedDNSHits.Value(); v != 0 {
t.Errorf("got hits=%d; want 0", v)
}
if v := unpublishedDNSMisses.Value(); v != 1 {
t.Errorf("got misses=%d; want 1", v)
}
}
})
// Verify that we do get a valid response and metric.
t.Run("CacheHit", func(t *testing.T) {
resetMetrics()
ips := getBootstrapDNS(t, "controlplane.tailscale.com")
want := dnsEntryMap{"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)}}
if !reflect.DeepEqual(ips, want) {
t.Errorf("got ips=%+v; want %+v", ips, want)
}
if v := unpublishedDNSHits.Value(); v != 1 {
t.Errorf("got hits=%d; want 1", v)
}
if v := unpublishedDNSMisses.Value(); v != 0 {
t.Errorf("got misses=%d; want 0", v)
}
})
}

View File

@@ -2,13 +2,16 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
LW github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
@@ -34,7 +37,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/hostinfo from tailscale.com/net/interfaces+
tailscale.com/ipn from tailscale.com/client/tailscale
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
💣 tailscale.com/metrics from tailscale.com/cmd/derper+
tailscale.com/metrics from tailscale.com/cmd/derper+
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
@@ -47,11 +50,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/paths from tailscale.com/client/tailscale
tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
@@ -63,19 +67,21 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/logger from tailscale.com/cmd/derper+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/client/tailscale+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/ipn
tailscale.com/types/ptr from tailscale.com/hostinfo
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/types/key+
tailscale.com/types/views from tailscale.com/ipn/ipnstate+
W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/syncs
tailscale.com/util/singleflight from tailscale.com/net/dnscache
L tailscale.com/util/strs from tailscale.com/hostinfo
tailscale.com/util/strs from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
@@ -94,6 +100,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
@@ -107,6 +115,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
@@ -176,6 +186,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/url from crypto/x509+
os from crypto/rand+
os/exec from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W os/user from tailscale.com/util/winutil
path from golang.org/x/crypto/acme/autocert+
path/filepath from crypto/x509+
reflect from crypto/x509+

View File

@@ -14,7 +14,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
@@ -26,6 +25,7 @@ import (
"strings"
"time"
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/atomicfile"
"tailscale.com/derp"
@@ -46,11 +46,13 @@ var (
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
@@ -96,7 +98,7 @@ func loadConfig() config {
}
log.Printf("no config path specified; using %s", *configPath)
}
b, err := ioutil.ReadFile(*configPath)
b, err := os.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
return writeNewConfig()
@@ -152,7 +154,7 @@ func main() {
s.SetVerifyClient(*verifyClients)
if *meshPSKFile != "" {
b, err := ioutil.ReadFile(*meshPSKFile)
b, err := os.ReadFile(*meshPSKFile)
if err != nil {
log.Fatal(err)
}
@@ -169,9 +171,15 @@ func main() {
expvar.Publish("derp", s.ExpVar())
mux := http.NewServeMux()
derpHandler := derphttp.Handler(s)
derpHandler = addWebSocketSupport(s, derpHandler)
mux.Handle("/derp", derpHandler)
if *runDERP {
derpHandler := derphttp.Handler(s)
derpHandler = addWebSocketSupport(s, derpHandler)
mux.Handle("/derp", derpHandler)
} else {
mux.Handle("/derp", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "derp server disabled", http.StatusNotFound)
}))
}
mux.HandleFunc("/derp/probe", probeHandler)
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
@@ -187,10 +195,17 @@ func main() {
server.
</p>
`)
if !*runDERP {
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
}
if tsweb.AllowDebugAccess(r) {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "User-agent: *\nDisallow: /\n")
}))
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent))
debug := tsweb.Debugger(mux)
debug.KV("TLS hostname", *hostname)
debug.KV("Mesh key", s.HasMeshKey())
@@ -208,9 +223,11 @@ func main() {
go serveSTUN(listenHost, *stunPort)
}
quietLogger := log.New(logFilter{}, "", 0)
httpsrv := &http.Server{
Addr: *addr,
Handler: mux,
Addr: *addr,
Handler: mux,
ErrorLog: quietLogger,
// Set read/write timeout. For derper, this basically
// only affects TLS setup, as read/write deadlines are
@@ -276,9 +293,13 @@ func main() {
})
if *httpPort > -1 {
go func() {
port80mux := http.NewServeMux()
port80mux.HandleFunc("/generate_204", serveNoContent)
port80mux.Handle("/", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
Handler: port80mux,
ErrorLog: quietLogger,
ReadTimeout: 30 * time.Second,
// Crank up WriteTimeout a bit more than usually
// necessary just so we can do long CPU profiles
@@ -304,6 +325,31 @@ func main() {
}
}
const (
noContentChallengeHeader = "X-Tailscale-Challenge"
noContentResponseHeader = "X-Tailscale-Response"
)
// For captive portal detection
func serveNoContent(w http.ResponseWriter, r *http.Request) {
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
w.Header().Set(noContentResponseHeader, "response "+challenge)
}
}
w.WriteHeader(http.StatusNoContent)
}
func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
}
// probeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func probeHandler(w http.ResponseWriter, r *http.Request) {
@@ -449,3 +495,22 @@ func (l *rateLimitedListener) Accept() (net.Conn, error) {
l.numAccepts.Add(1)
return cn, nil
}
// logFilter is used to filter out useless error logs that are logged to
// the net/http.Server.ErrorLog logger.
type logFilter struct{}
func (logFilter) Write(p []byte) (int, error) {
b := mem.B(p)
if mem.HasSuffix(b, mem.S(": EOF\n")) ||
mem.HasSuffix(b, mem.S(": i/o timeout\n")) ||
mem.HasSuffix(b, mem.S(": read: connection reset by peer\n")) ||
mem.HasSuffix(b, mem.S(": remote error: tls: bad certificate\n")) ||
mem.HasSuffix(b, mem.S(": tls: first record does not look like a TLS handshake\n")) {
// Skip this log message, but say that we processed it
return len(p), nil
}
log.Printf("%s", p)
return len(p), nil
}

View File

@@ -7,6 +7,9 @@ package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tailscale.com/net/stun"
@@ -67,3 +70,62 @@ func BenchmarkServerSTUN(b *testing.B) {
}
}
func TestNoContent(t *testing.T) {
testCases := []struct {
name string
input string
want string
}{
{
name: "no challenge",
},
{
name: "valid challenge",
input: "input",
want: "response input",
},
{
name: "valid challenge hostname",
input: "ts_derp99b.tailscale.com",
want: "response ts_derp99b.tailscale.com",
},
{
name: "invalid challenge",
input: "foo\x00bar",
want: "",
},
{
name: "whitespace invalid challenge",
input: "foo bar",
want: "",
},
{
name: "long challenge",
input: strings.Repeat("x", 65),
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
if tt.input != "" {
req.Header.Set(noContentChallengeHeader, tt.input)
}
w := httptest.NewRecorder()
serveNoContent(w, req)
resp := w.Result()
if tt.want == "" {
if h, found := resp.Header[noContentResponseHeader]; found {
t.Errorf("got %+v; expected no response header", h)
}
return
}
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}

View File

@@ -17,6 +17,7 @@ import (
"tailscale.com/derp/derphttp"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/strs"
)
func startMesh(s *derp.Server) error {
@@ -50,8 +51,7 @@ func startMeshWithHost(s *derp.Server, host string) error {
}
var d net.Dialer
var r net.Resolver
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
base := strings.TrimSuffix(host, ".tailscale.com")
if base, ok := strs.CutSuffix(host, ".tailscale.com"); ok && port == "443" {
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
vpcHost := base + "-vpc.tailscale.com"

View File

@@ -13,6 +13,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/net/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
@@ -23,7 +24,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
up := strings.ToLower(r.Header.Get("Upgrade"))
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
base.ServeHTTP(w, r)
@@ -33,6 +34,12 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because we transmit WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Printf("websocket.Accept: %v", err)
@@ -44,7 +51,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return
}
counterWebSocketAccepts.Add(1)
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
})

View File

@@ -35,6 +35,7 @@ import (
var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
)
// certReissueAfter is the time after which we expect all certs to be
@@ -63,6 +64,20 @@ func main() {
defer cancel()
_, _ = getDERPMap(ctx)
if *probeOnce {
log.Printf("Starting probe (may take up to 1m)")
probe()
log.Printf("Probe results:")
st := getOverallStatus()
for _, s := range st.good {
log.Printf("good: %s", s)
}
for _, s := range st.bad {
log.Printf("bad: %s", s)
}
return
}
go probeLoop()
go slackLoop()
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))

View File

@@ -8,6 +8,7 @@
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
@@ -30,17 +31,15 @@ var (
cacheFname = rootFlagSet.String("cache-file", "./version-cache.json", "filename for the previous known version hash")
timeout = rootFlagSet.Duration("timeout", 5*time.Minute, "timeout for the entire CI run")
githubSyntax = rootFlagSet.Bool("github-syntax", true, "use GitHub Action error syntax (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message)")
modifiedExternallyFailure = make(chan struct{}, 1)
apiServer = rootFlagSet.String("api-server", "api.tailscale.com", "API server to contact")
)
func modifiedExternallyError() {
if *githubSyntax {
fmt.Printf("::error file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
fmt.Printf("::warning file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
} else {
fmt.Printf("The policy file was modified externally in the admin console.\n")
}
modifiedExternallyFailure <- struct{}{}
}
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
@@ -207,10 +206,6 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
if len(modifiedExternallyFailure) != 0 {
os.Exit(1)
}
}
func sumFile(fname string) (string, error) {
@@ -240,7 +235,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
}
defer fin.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), fin)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), fin)
if err != nil {
return err
}
@@ -271,13 +266,16 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
}
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
fin, err := os.Open(policyFname)
data, err := os.ReadFile(policyFname)
if err != nil {
return err
}
data, err = hujson.Standardize(data)
if err != nil {
return err
}
defer fin.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), fin)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl/validate", *apiServer, tailnet), bytes.NewBuffer(data))
if err != nil {
return err
}
@@ -349,7 +347,7 @@ type ACLTestErrorDetail struct {
}
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
if err != nil {
return "", err
}

View File

@@ -13,7 +13,6 @@ import (
"errors"
"flag"
"html/template"
"io/ioutil"
"log"
"net/http"
"os"
@@ -106,7 +105,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
tmplData, err := os.ReadFile("hello.tmpl.html")
if os.IsNotExist(err) {
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
return tmpl, nil

View File

@@ -0,0 +1,155 @@
# Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
apiVersion: v1
kind: Namespace
metadata:
name: tailscale
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: proxies
namespace: tailscale
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: proxies
namespace: tailscale
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: proxies
namespace: tailscale
subjects:
- kind: ServiceAccount
name: proxies
namespace: tailscale
roleRef:
kind: Role
name: proxies
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: operator
namespace: tailscale
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tailscale-operator
rules:
- apiGroups: [""]
resources: ["services", "services/status"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tailscale-operator
subjects:
- kind: ServiceAccount
name: operator
namespace: tailscale
roleRef:
kind: ClusterRole
name: tailscale-operator
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: operator
namespace: tailscale
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["*"]
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: operator
namespace: tailscale
subjects:
- kind: ServiceAccount
name: operator
namespace: tailscale
roleRef:
kind: Role
name: operator
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Secret
metadata:
name: operator-oauth
namespace: tailscale
stringData:
client_id: # SET CLIENT ID HERE
client_secret: # SET CLIENT SECRET HERE
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: operator
namespace: tailscale
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: operator
template:
metadata:
labels:
app: operator
spec:
serviceAccountName: operator
volumes:
- name: oauth
secret:
secretName: operator-oauth
containers:
- name: operator
image: tailscale/k8s-operator:latest
resources:
requests:
cpu: 500m
memory: 100Mi
env:
- name: OPERATOR_HOSTNAME
value: tailscale-operator
- name: OPERATOR_SECRET
value: operator
- name: OPERATOR_LOGGING
value: info
- name: OPERATOR_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CLIENT_ID_FILE
value: /oauth/client_id
- name: CLIENT_SECRET_FILE
value: /oauth/client_secret
- name: PROXY_IMAGE
value: tailscale/tailscale:latest
- name: PROXY_TAGS
value: tag:k8s
volumeMounts:
- name: oauth
mountPath: /oauth
readOnly: true

View File

@@ -0,0 +1,37 @@
# This file is not a complete manifest, it's a skeleton that the operator embeds
# at build time and then uses to construct Tailscale proxy pods.
apiVersion: apps/v1
kind: StatefulSet
metadata:
spec:
replicas: 1
template:
metadata:
deletionGracePeriodSeconds: 10
spec:
serviceAccountName: proxies
initContainers:
- name: sysctler
image: busybox
securityContext:
privileged: true
command: ["/bin/sh"]
args:
- -c
- sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
resources:
requests:
cpu: 1m
memory: 1Mi
containers:
- name: tailscale
imagePullPolicy: Always
env:
- name: TS_USERSPACE
value: "false"
- name: TS_AUTH_ONCE
value: "true"
securityContext:
capabilities:
add:
- NET_ADMIN

View File

@@ -0,0 +1,685 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// tailscale-operator provides a way to expose services running in a Kubernetes
// cluster to your Tailnet.
package main
import (
"context"
_ "embed"
"fmt"
"os"
"strings"
"time"
"github.com/go-logr/zapr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/exp/slices"
"golang.org/x/oauth2/clientcredentials"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
kzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/store/kubestore"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
)
func main() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
var (
hostname = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
kubeSecret = defaultEnv("OPERATOR_SECRET", "")
operatorTags = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
)
var opts []kzap.Opts
switch tslogging {
case "info":
opts = append(opts, kzap.Level(zapcore.InfoLevel))
case "debug":
opts = append(opts, kzap.Level(zapcore.DebugLevel))
case "dev":
opts = append(opts, kzap.UseDevMode(true), kzap.Level(zapcore.DebugLevel))
}
zlog := kzap.NewRaw(opts...).Sugar()
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
startlog := zlog.Named("startup")
if clientIDPath == "" || clientSecretPath == "" {
startlog.Fatalf("CLIENT_ID_FILE and CLIENT_SECRET_FILE must be set")
}
clientID, err := os.ReadFile(clientIDPath)
if err != nil {
startlog.Fatalf("reading client ID %q: %v", clientIDPath, err)
}
clientSecret, err := os.ReadFile(clientSecretPath)
if err != nil {
startlog.Fatalf("reading client secret %q: %v", clientSecretPath, err)
}
credentials := clientcredentials.Config{
ClientID: string(clientID),
ClientSecret: string(clientSecret),
TokenURL: "https://login.tailscale.com/api/v2/oauth/token",
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(context.Background())
s := &tsnet.Server{
Hostname: hostname,
Logf: zlog.Named("tailscaled").Debugf,
}
if kubeSecret != "" {
st, err := kubestore.New(logger.Discard, kubeSecret)
if err != nil {
startlog.Fatalf("creating kube store: %v", err)
}
s.Store = st
}
if err := s.Start(); err != nil {
startlog.Fatalf("starting tailscale server: %v", err)
}
defer s.Close()
lc, err := s.LocalClient()
if err != nil {
startlog.Fatalf("getting local client: %v", err)
}
ctx := context.Background()
loginDone := false
machineAuthShown := false
waitOnline:
for {
startlog.Debugf("querying tailscaled status")
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
startlog.Fatalf("getting status: %v", err)
}
switch st.BackendState {
case "Running":
break waitOnline
case "NeedsLogin":
if loginDone {
break
}
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Preauthorized: true,
Tags: strings.Split(operatorTags, ","),
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
startlog.Fatalf("creating operator authkey: %v", err)
}
if err := lc.Start(ctx, ipn.Options{
AuthKey: authkey,
}); err != nil {
startlog.Fatalf("starting tailscale: %v", err)
}
if err := lc.StartLoginInteractive(ctx); err != nil {
startlog.Fatalf("starting login: %v", err)
}
startlog.Debugf("requested login by authkey")
loginDone = true
case "NeedsMachineAuth":
if !machineAuthShown {
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
machineAuthShown = true
}
default:
startlog.Debugf("waiting for tailscale to start: %v", st.BackendState)
}
time.Sleep(time.Second)
}
sr := &ServiceReconciler{
tsClient: tsClient,
defaultTags: strings.Split(tags, ","),
operatorNamespace: tsNamespace,
proxyImage: image,
logger: zlog.Named("service-reconciler"),
}
// For secrets and statefulsets, we only get permission to touch the objects
// in the controller's own namespace. This cannot be expressed by
// .Watches(...) below, instead you have to add a per-type field selector to
// the cache that sits a few layers below the builder stuff, which will
// implicitly filter what parts of the world the builder code gets to see at
// all.
nsFilter := cache.ObjectSelector{
Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
}
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{
NewCache: cache.BuilderWithOptions(cache.Options{
SelectorsByObject: map[client.Object]cache.ObjectSelector{
&corev1.Secret{}: nsFilter,
&appsv1.StatefulSet{}: nsFilter,
},
}),
})
if err != nil {
startlog.Fatalf("could not create manager: %v", err)
}
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
ls := o.GetLabels()
if ls[LabelManaged] != "true" {
return nil
}
if ls[LabelParentType] != "svc" {
return nil
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: ls[LabelParentNamespace],
Name: ls[LabelParentName],
},
},
}
})
err = builder.
ControllerManagedBy(mgr).
For(&corev1.Service{}).
Watches(&source.Kind{Type: &appsv1.StatefulSet{}}, reconcileFilter).
Watches(&source.Kind{Type: &corev1.Secret{}}, reconcileFilter).
Complete(sr)
if err != nil {
startlog.Fatalf("could not create controller: %v", err)
}
startlog.Infof("Startup complete, operator running")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
startlog.Fatalf("could not start manager: %v", err)
}
}
const (
LabelManaged = "tailscale.com/managed"
LabelParentType = "tailscale.com/parent-resource-type"
LabelParentName = "tailscale.com/parent-resource"
LabelParentNamespace = "tailscale.com/parent-resource-ns"
FinalizerName = "tailscale.com/finalizer"
AnnotationExpose = "tailscale.com/expose"
AnnotationTags = "tailscale.com/tags"
)
// ServiceReconciler is a simple ControllerManagedBy example implementation.
type ServiceReconciler struct {
client.Client
tsClient tsClient
defaultTags []string
operatorNamespace string
proxyImage string
logger *zap.SugaredLogger
}
type tsClient interface {
CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error)
DeleteDevice(ctx context.Context, id string) error
}
func childResourceLabels(parent *corev1.Service) map[string]string {
// You might wonder why we're using owner references, since they seem to be
// built for exactly this. Unfortunately, Kubernetes does not support
// cross-namespace ownership, by design. This means we cannot make the
// service being exposed the owner of the implementation details of the
// proxying. Instead, we have to do our own filtering and tracking with
// labels.
return map[string]string{
LabelManaged: "true",
LabelParentName: parent.GetName(),
LabelParentNamespace: parent.GetNamespace(),
LabelParentType: "svc",
}
}
func (a *ServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
logger := a.logger.With("service-ns", req.Namespace, "service-name", req.Name)
logger.Debugf("starting reconcile")
defer logger.Debugf("reconcile finished")
svc := new(corev1.Service)
err = a.Get(ctx, req.NamespacedName, svc)
if apierrors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
logger.Debugf("service not found, assuming it was deleted")
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get svc: %w", err)
}
if !svc.DeletionTimestamp.IsZero() || !a.shouldExpose(svc) {
logger.Debugf("service is being deleted or should not be exposed, cleaning up")
return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc)
}
return reconcile.Result{}, a.maybeProvision(ctx, logger, svc)
}
// maybeCleanup removes any existing resources related to serving svc over tailscale.
//
// This function is responsible for removing the finalizer from the service,
// once all associated resources are gone.
func (a *ServiceReconciler) maybeCleanup(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) error {
ix := slices.Index(svc.Finalizers, FinalizerName)
if ix < 0 {
logger.Debugf("no finalizer, nothing to do")
return nil
}
ml := childResourceLabels(svc)
// Need to delete the StatefulSet first, and delete it with foreground
// cascading deletion. That way, the pod that's writing to the Secret will
// stop running before we start looking at the Secret's contents, and
// assuming k8s ordering semantics don't mess with us, that should avoid
// tailscale device deletion races where we fail to notice a device that
// should be removed.
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, ml)
if err != nil {
return fmt.Errorf("getting statefulset: %w", err)
}
if sts != nil {
if !sts.GetDeletionTimestamp().IsZero() {
// Deletion in progress, check again later. We'll get another
// notification when the deletion is complete.
logger.Debugf("waiting for statefulset %s/%s deletion", sts.GetNamespace(), sts.GetName())
return nil
}
err := a.DeleteAllOf(ctx, &appsv1.StatefulSet{}, client.InNamespace(a.operatorNamespace), client.MatchingLabels(ml), client.PropagationPolicy(metav1.DeletePropagationForeground))
if err != nil {
return fmt.Errorf("deleting statefulset: %w", err)
}
logger.Debugf("started deletion of statefulset %s/%s", sts.GetNamespace(), sts.GetName())
return nil
}
id, _, err := a.getDeviceInfo(ctx, svc)
if err != nil {
return fmt.Errorf("getting device info: %w", err)
}
if id != "" {
// TODO: handle case where the device is already deleted, but the secret
// is still around.
if err := a.tsClient.DeleteDevice(ctx, id); err != nil {
return fmt.Errorf("deleting device: %w", err)
}
}
types := []client.Object{
&corev1.Service{},
&corev1.Secret{},
}
for _, typ := range types {
if err := a.DeleteAllOf(ctx, typ, client.InNamespace(a.operatorNamespace), client.MatchingLabels(ml)); err != nil {
return err
}
}
svc.Finalizers = append(svc.Finalizers[:ix], svc.Finalizers[ix+1:]...)
if err := a.Update(ctx, svc); err != nil {
return fmt.Errorf("failed to remove finalizer: %w", err)
}
// Unlike most log entries in the reconcile loop, this will get printed
// exactly once at the very end of cleanup, because the final step of
// cleanup removes the tailscale finalizer, which will make all future
// reconciles exit early.
logger.Infof("unexposed service from tailnet")
return nil
}
// maybeProvision ensures that svc is exposed over tailscale, taking any actions
// necessary to reach that state.
//
// This function adds a finalizer to svc, ensuring that we can handle orderly
// deprovisioning later.
func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) error {
if !slices.Contains(svc.Finalizers, FinalizerName) {
// This log line is printed exactly once during initial provisioning,
// because once the finalizer is in place this block gets skipped. So,
// this is a nice place to tell the operator that the high level,
// multi-reconcile operation is underway.
logger.Infof("exposing service over tailscale")
svc.Finalizers = append(svc.Finalizers, FinalizerName)
if err := a.Update(ctx, svc); err != nil {
return fmt.Errorf("failed to add finalizer: %w", err)
}
}
// Do full reconcile.
hsvc, err := a.reconcileHeadlessService(ctx, logger, svc)
if err != nil {
return fmt.Errorf("failed to reconcile headless service: %w", err)
}
tags := a.defaultTags
if tstr, ok := svc.Annotations[AnnotationTags]; ok {
tags = strings.Split(tstr, ",")
}
secretName, err := a.createOrGetSecret(ctx, logger, svc, hsvc, tags)
if err != nil {
return fmt.Errorf("failed to create or get API key secret: %w", err)
}
_, err = a.reconcileSTS(ctx, logger, svc, hsvc, secretName)
if err != nil {
return fmt.Errorf("failed to reconcile statefulset: %w", err)
}
if !a.hasLoadBalancerClass(svc) {
logger.Debugf("service is not a LoadBalancer, so not updating ingress")
return nil
}
_, tsHost, err := a.getDeviceInfo(ctx, svc)
if err != nil {
return fmt.Errorf("failed to get device ID: %w", err)
}
if tsHost == "" {
logger.Debugf("no Tailscale hostname known yet, waiting for proxy pod to finish auth")
// No hostname yet. Wait for the proxy pod to auth.
svc.Status.LoadBalancer.Ingress = nil
if err := a.Status().Update(ctx, svc); err != nil {
return fmt.Errorf("failed to update service status: %w", err)
}
return nil
}
logger.Debugf("setting ingress hostname to %q", tsHost)
svc.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{
{
Hostname: tsHost,
},
}
if err := a.Status().Update(ctx, svc); err != nil {
return fmt.Errorf("failed to update service status: %w", err)
}
return nil
}
func (a *ServiceReconciler) shouldExpose(svc *corev1.Service) bool {
// Headless services can't be exposed, since there is no ClusterIP to
// forward to.
if svc.Spec.ClusterIP == "" || svc.Spec.ClusterIP == "None" {
return false
}
return a.hasLoadBalancerClass(svc) || a.hasAnnotation(svc)
}
func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
return svc != nil &&
svc.Spec.Type == corev1.ServiceTypeLoadBalancer &&
svc.Spec.LoadBalancerClass != nil &&
*svc.Spec.LoadBalancerClass == "tailscale"
}
func (a *ServiceReconciler) hasAnnotation(svc *corev1.Service) bool {
return svc != nil &&
svc.Annotations[AnnotationExpose] == "true"
}
func (a *ServiceReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) (*corev1.Service, error) {
hsvc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "ts-" + svc.Name + "-",
Namespace: a.operatorNamespace,
Labels: childResourceLabels(svc),
},
Spec: corev1.ServiceSpec{
ClusterIP: "None",
Selector: map[string]string{
"app": string(svc.UID),
},
},
}
logger.Debugf("reconciling headless service for StatefulSet")
return createOrUpdate(ctx, a.Client, a.operatorNamespace, hsvc, func(svc *corev1.Service) { svc.Spec = hsvc.Spec })
}
func (a *ServiceReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, svc, hsvc *corev1.Service, tags []string) (string, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
// Hardcode a -0 suffix so that in future, if we support
// multiple StatefulSet replicas, we can provision -N for
// those.
Name: hsvc.Name + "-0",
Namespace: a.operatorNamespace,
Labels: childResourceLabels(svc),
},
}
if err := a.Get(ctx, client.ObjectKeyFromObject(secret), secret); err == nil {
logger.Debugf("secret %s/%s already exists", secret.GetNamespace(), secret.GetName())
return secret.Name, nil
} else if !apierrors.IsNotFound(err) {
return "", err
}
// Secret doesn't exist yet, create one. Initially it contains
// only the Tailscale authkey, but once Tailscale starts it'll
// also store the daemon state.
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, childResourceLabels(svc))
if err != nil {
return "", err
}
if sts != nil {
// StatefulSet exists, so we have already created the secret.
// If the secret is missing, they should delete the StatefulSet.
logger.Errorf("Tailscale proxy secret doesn't exist, but the corresponding StatefulSet %s/%s already does. Something is wrong, please delete the StatefulSet.", sts.GetNamespace(), sts.GetName())
return "", nil
}
// Create API Key secret which is going to be used by the statefulset
// to authenticate with Tailscale.
logger.Debugf("creating authkey for new tailscale proxy")
authKey, err := a.newAuthKey(ctx, tags)
if err != nil {
return "", err
}
secret.StringData = map[string]string{
"authkey": authKey,
}
if err := a.Create(ctx, secret); err != nil {
return "", err
}
return secret.Name, nil
}
func (a *ServiceReconciler) getDeviceInfo(ctx context.Context, svc *corev1.Service) (id, hostname string, err error) {
sec, err := getSingleObject[corev1.Secret](ctx, a.Client, a.operatorNamespace, childResourceLabels(svc))
if err != nil {
return "", "", err
}
id = string(sec.Data["device_id"])
if id == "" {
return "", "", nil
}
// Kubernetes chokes on well-formed FQDNs with the trailing dot, so we have
// to remove it.
hostname = strings.TrimSuffix(string(sec.Data["device_fqdn"]), ".")
if hostname == "" {
return "", "", nil
}
return id, hostname, nil
}
func (a *ServiceReconciler) newAuthKey(ctx context.Context, tags []string) (string, error) {
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Preauthorized: true,
Tags: tags,
},
},
}
key, _, err := a.tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return key, nil
}
//go:embed manifests/proxy.yaml
var proxyYaml []byte
func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, parentSvc, headlessSvc *corev1.Service, authKeySecret string) (*appsv1.StatefulSet, error) {
var ss appsv1.StatefulSet
if err := yaml.Unmarshal(proxyYaml, &ss); err != nil {
return nil, fmt.Errorf("failed to unmarshal proxy spec: %w", err)
}
container := &ss.Spec.Template.Spec.Containers[0]
container.Image = a.proxyImage
container.Env = append(container.Env,
corev1.EnvVar{
Name: "TS_DEST_IP",
Value: parentSvc.Spec.ClusterIP,
},
corev1.EnvVar{
Name: "TS_KUBE_SECRET",
Value: authKeySecret,
})
ss.ObjectMeta = metav1.ObjectMeta{
Name: headlessSvc.Name,
Namespace: a.operatorNamespace,
Labels: childResourceLabels(parentSvc),
}
ss.Spec.ServiceName = headlessSvc.Name
ss.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": string(parentSvc.UID),
},
}
ss.Spec.Template.ObjectMeta.Labels = map[string]string{
"app": string(parentSvc.UID),
}
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
}
func (a *ServiceReconciler) InjectClient(c client.Client) error {
a.Client = c
return nil
}
// ptrObject is a type constraint for pointer types that implement
// client.Object.
type ptrObject[T any] interface {
client.Object
*T
}
// createOrUpdate adds obj to the k8s cluster, unless the object already exists,
// in which case update is called to make changes to it. If update is nil, the
// existing object is returned unmodified.
//
// obj is looked up by its Name and Namespace if Name is set, otherwise it's
// looked up by labels.
func createOrUpdate[T any, O ptrObject[T]](ctx context.Context, c client.Client, ns string, obj O, update func(O)) (O, error) {
var (
existing O
err error
)
if obj.GetName() != "" {
existing = new(T)
existing.SetName(obj.GetName())
existing.SetNamespace(obj.GetNamespace())
err = c.Get(ctx, client.ObjectKeyFromObject(obj), existing)
} else {
existing, err = getSingleObject[T, O](ctx, c, ns, obj.GetLabels())
}
if err == nil && existing != nil {
if update != nil {
update(existing)
if err := c.Update(ctx, existing); err != nil {
return nil, err
}
}
return existing, nil
}
if err != nil && !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("failed to get object: %w", err)
}
if err := c.Create(ctx, obj); err != nil {
return nil, err
}
return obj, nil
}
// getSingleObject searches for k8s objects of type T
// (e.g. corev1.Service) with the given labels, and returns
// it. Returns nil if no objects match the labels, and an error if
// more than one object matches.
func getSingleObject[T any, O ptrObject[T]](ctx context.Context, c client.Client, ns string, labels map[string]string) (O, error) {
ret := O(new(T))
kinds, _, err := c.Scheme().ObjectKinds(ret)
if err != nil {
return nil, err
}
if len(kinds) != 1 {
// TODO: the runtime package apparently has a "pick the best
// GVK" function somewhere that might be good enough?
return nil, fmt.Errorf("more than 1 GroupVersionKind for %T", ret)
}
gvk := kinds[0]
gvk.Kind += "List"
lst := unstructured.UnstructuredList{}
lst.SetGroupVersionKind(gvk)
if err := c.List(ctx, &lst, client.InNamespace(ns), client.MatchingLabels(labels)); err != nil {
return nil, err
}
if len(lst.Items) == 0 {
return nil, nil
}
if len(lst.Items) > 1 {
return nil, fmt.Errorf("found multiple matching %T objects", ret)
}
if err := c.Scheme().Convert(&lst.Items[0], ret, nil); err != nil {
return nil, err
}
return ret, nil
}
func defaultEnv(envName, defVal string) string {
v := os.Getenv(envName)
if v == "" {
return defVal
}
return v
}

View File

@@ -0,0 +1,739 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
"tailscale.com/types/ptr"
)
func TestLoadBalancerClass(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
// that we get to the end.
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
if s.Data == nil {
s.Data = map[string][]byte{}
}
s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
})
expectReconciled(t, sr, "default", "test")
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
Hostname: "tailscale.device.name",
},
},
},
},
}
expectEqual(t, fc, want)
// Turn the service back into a ClusterIP service, which should make the
// operator clean up.
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
s.Spec.Type = corev1.ServiceTypeClusterIP
s.Spec.LoadBalancerClass = nil
// Fake client doesn't automatically delete the LoadBalancer status when
// changing away from the LoadBalancer type, we have to do
// controller-manager's work by hand.
s.Status = corev1.ServiceStatus{}
})
// synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
// didn't create any child resources since this is all faked, so the
// deletion goes through immediately.
expectReconciled(t, sr, "default", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
// The deletion triggers another reconcile, to finish the cleanup.
expectReconciled(t, sr, "default", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
}
func TestAnnotations(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
// Turn the service back into a ClusterIP service, which should make the
// operator clean up.
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
})
// synchronous StatefulSet deletion triggers a requeue. But, the StatefulSet
// didn't create any child resources since this is all faked, so the
// deletion goes through immediately.
expectReconciled(t, sr, "default", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
// Second time around, the rest of cleanup happens.
expectReconciled(t, sr, "default", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
}
func TestAnnotationIntoLB(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, since it would have normally happened at
// this point and the LoadBalancer is going to expect this.
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
if s.Data == nil {
s.Data = map[string][]byte{}
}
s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
})
expectReconciled(t, sr, "default", "test")
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
// Remove Tailscale's annotation, and at the same time convert the service
// into a tailscale LoadBalancer.
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
delete(s.ObjectMeta.Annotations, "tailscale.com/expose")
s.Spec.Type = corev1.ServiceTypeLoadBalancer
s.Spec.LoadBalancerClass = ptr.To("tailscale")
})
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
// ... but the service should have a LoadBalancer status.
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
Hostname: "tailscale.device.name",
},
},
},
},
}
expectEqual(t, fc, want)
}
func TestLBIntoAnnotation(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
// that we get to the end.
mustUpdate(t, fc, "operator-ns", fullName, func(s *corev1.Secret) {
if s.Data == nil {
s.Data = map[string][]byte{}
}
s.Data["device_id"] = []byte("ts-id-1234")
s.Data["device_fqdn"] = []byte("tailscale.device.name.")
})
expectReconciled(t, sr, "default", "test")
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
Hostname: "tailscale.device.name",
},
},
},
},
}
expectEqual(t, fc, want)
// Turn the service back into a ClusterIP service, but also add the
// tailscale annotation.
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
s.ObjectMeta.Annotations = map[string]string{
"tailscale.com/expose": "true",
}
s.Spec.Type = corev1.ServiceTypeClusterIP
s.Spec.LoadBalancerClass = nil
// Fake client doesn't automatically delete the LoadBalancer status when
// changing away from the LoadBalancer type, we have to do
// controller-manager's work by hand.
s.Status = corev1.ServiceStatus{}
})
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName))
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{"tailscale.com/finalizer"},
Annotations: map[string]string{
"tailscale.com/expose": "true",
},
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
}
func expectedSecret(name string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
StringData: map[string]string{
"authkey": "secret-authkey",
},
}
}
func expectedHeadlessService(name string) *corev1.Service {
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
GenerateName: "ts-test-",
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "1234-UID",
},
ClusterIP: "None",
},
}
}
func expectedSTS(stsName, secretName string) *appsv1.StatefulSet {
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: stsName,
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
Spec: appsv1.StatefulSetSpec{
Replicas: ptr.To[int32](1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "1234-UID"},
},
ServiceName: stsName,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
DeletionGracePeriodSeconds: ptr.To[int64](10),
Labels: map[string]string{"app": "1234-UID"},
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",
InitContainers: []corev1.Container{
{
Name: "sysctler",
Image: "busybox",
Command: []string{"/bin/sh"},
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
},
},
Containers: []v1.Container{
{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []v1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
{Name: "TS_KUBE_SECRET", Value: secretName},
},
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
ImagePullPolicy: "Always",
},
},
},
},
},
}
}
func findGenName(t *testing.T, client client.Client, ns, name string) (full, noSuffix string) {
t.Helper()
labels := map[string]string{
LabelManaged: "true",
LabelParentName: name,
LabelParentNamespace: ns,
LabelParentType: "svc",
}
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
if err != nil {
t.Fatalf("finding secret for %q: %v", name, err)
}
return s.GetName(), strings.TrimSuffix(s.GetName(), "-0")
}
func mustCreate(t *testing.T, client client.Client, obj client.Object) {
t.Helper()
if err := client.Create(context.Background(), obj); err != nil {
t.Fatalf("creating %q: %v", obj.GetName(), err)
}
}
func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
t.Helper()
got := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: want.GetName(),
Namespace: want.GetNamespace(),
}, got); err != nil {
t.Fatalf("getting %q: %v", want.GetName(), err)
}
// The resource version changes eagerly whenever the operator does even a
// no-op update. Asserting a specific value leads to overly brittle tests,
// so just remove it from both got and want.
got.SetResourceVersion("")
want.SetResourceVersion("")
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("unexpected object (-got +want):\n%s", diff)
}
}
func expectMissing[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); !apierrors.IsNotFound(err) {
t.Fatalf("object %s/%s unexpectedly present, wanted missing", ns, name)
}
}
func expectReconciled(t *testing.T, sr *ServiceReconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: ns,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected immediate requeue")
}
if res.RequeueAfter != 0 {
t.Fatalf("unexpected timed requeue (%v)", res.RequeueAfter)
}
}
func expectRequeue(t *testing.T, sr *ServiceReconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: ns,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected immediate requeue")
}
if res.RequeueAfter == 0 {
t.Fatalf("expected timed requeue, got success")
}
}
type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
deleted []string
}
func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
c.Lock()
defer c.Unlock()
c.keyRequests = append(c.keyRequests, caps)
k := &tailscale.Key{
ID: "key",
Created: time.Now(),
Expires: time.Now().Add(24 * time.Hour),
Capabilities: caps,
}
return "secret-authkey", k, nil
}
func (c *fakeTSClient) DeleteDevice(ctx context.Context, deviceID string) error {
c.Lock()
defer c.Unlock()
c.deleted = append(c.deleted, deviceID)
return nil
}
func (c *fakeTSClient) KeyRequests() []tailscale.KeyCapabilities {
c.Lock()
defer c.Unlock()
return c.keyRequests
}
func (c *fakeTSClient) Deleted() []string {
c.Lock()
defer c.Unlock()
return c.deleted
}

52
cmd/mkmanifest/main.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The mkmanifest command is a simple helper utility to create a '.syso' file
// that contains a Windows manifest file.
package main
import (
"log"
"os"
"github.com/tc-hib/winres"
)
func main() {
if len(os.Args) != 4 {
log.Fatalf("usage: %s arch manifest.xml output.syso", os.Args[0])
}
arch := winres.Arch(os.Args[1])
switch arch {
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
default:
log.Fatalf("unsupported arch: %s", arch)
}
manifest, err := os.ReadFile(os.Args[2])
if err != nil {
log.Fatalf("error reading manifest file %q: %v", os.Args[2], err)
}
out := os.Args[3]
// Start by creating an empty resource set
rs := winres.ResourceSet{}
// Add resources
rs.Set(winres.RT_MANIFEST, winres.ID(1), 0, manifest)
// Compile to a COFF object file
f, err := os.Create(out)
if err != nil {
log.Fatalf("error creating output file %q: %v", out, err)
}
if err := rs.WriteObject(f, arch); err != nil {
log.Fatalf("error writing object: %v", err)
}
if err := f.Close(); err != nil {
log.Fatalf("error writing output file %q: %v", out, err)
}
}

7
cmd/nardump/README.md Normal file
View File

@@ -0,0 +1,7 @@
# nardump
nardump is like nix-store --dump, but in Go, writing a NAR file (tar-like,
but focused on being reproducible) to stdout or to a hash with the --sri flag.
It lets us calculate the Nix sha256 in shell.nix without the person running
git-pull-oss.sh having Nix available.

185
cmd/nardump/nardump.go Normal file
View File

@@ -0,0 +1,185 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// nardump is like nix-store --dump, but in Go, writing a NAR
// file (tar-like, but focused on being reproducible) to stdout
// or to a hash with the --sri flag.
//
// It lets us calculate a Nix sha256 without the person running
// git-pull-oss.sh having Nix available.
package main
// For the format, see:
// See https://gist.github.com/jbeda/5c79d2b1434f0018d693
import (
"bufio"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"flag"
"fmt"
"io"
"io/fs"
"log"
"os"
"path"
"sort"
)
var sri = flag.Bool("sri", false, "print SRI")
func main() {
flag.Parse()
if flag.NArg() != 1 {
log.Fatal("usage: nardump <dir>")
}
arg := flag.Arg(0)
if err := os.Chdir(arg); err != nil {
log.Fatal(err)
}
if *sri {
hash := sha256.New()
if err := writeNAR(hash, os.DirFS(".")); err != nil {
log.Fatal(err)
}
fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
return
}
bw := bufio.NewWriter(os.Stdout)
if err := writeNAR(bw, os.DirFS(".")); err != nil {
log.Fatal(err)
}
bw.Flush()
}
// writeNARError is a sentinel panic type that's recovered by writeNAR
// and converted into the wrapped error.
type writeNARError struct{ err error }
// narWriter writes NAR files.
type narWriter struct {
w io.Writer
fs fs.FS
}
// writeNAR writes a NAR file to w from the root of fs.
func writeNAR(w io.Writer, fs fs.FS) (err error) {
defer func() {
if e := recover(); e != nil {
if we, ok := e.(writeNARError); ok {
err = we.err
return
}
panic(e)
}
}()
nw := &narWriter{w: w, fs: fs}
nw.str("nix-archive-1")
return nw.writeDir(".")
}
func (nw *narWriter) writeDir(dirPath string) error {
ents, err := fs.ReadDir(nw.fs, dirPath)
if err != nil {
return err
}
sort.Slice(ents, func(i, j int) bool {
return ents[i].Name() < ents[j].Name()
})
nw.str("(")
nw.str("type")
nw.str("directory")
for _, ent := range ents {
nw.str("entry")
nw.str("(")
nw.str("name")
nw.str(ent.Name())
nw.str("node")
mode := ent.Type()
sub := path.Join(dirPath, ent.Name())
var err error
switch {
case mode.IsRegular():
err = nw.writeRegular(sub)
case mode.IsDir():
err = nw.writeDir(sub)
default:
// TODO(bradfitz): symlink, but requires fighting io/fs a bit
// to get at Readlink or the osFS via fs. But for now
// we don't need symlinks because they're not in Go's archive.
return fmt.Errorf("unsupported file type %v at %q", sub, mode)
}
if err != nil {
return err
}
nw.str(")")
}
nw.str(")")
return nil
}
func (nw *narWriter) writeRegular(path string) error {
nw.str("(")
nw.str("type")
nw.str("regular")
fi, err := fs.Stat(nw.fs, path)
if err != nil {
return err
}
if fi.Mode()&0111 != 0 {
nw.str("executable")
nw.str("")
}
contents, err := fs.ReadFile(nw.fs, path)
if err != nil {
return err
}
nw.str("contents")
if err := writeBytes(nw.w, contents); err != nil {
return err
}
nw.str(")")
return nil
}
func (nw *narWriter) str(s string) {
if err := writeString(nw.w, s); err != nil {
panic(writeNARError{err})
}
}
func writeString(w io.Writer, s string) error {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
if _, err := w.Write(buf[:]); err != nil {
return err
}
if _, err := io.WriteString(w, s); err != nil {
return err
}
return writePad(w, len(s))
}
func writeBytes(w io.Writer, b []byte) error {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
if _, err := w.Write(buf[:]); err != nil {
return err
}
if _, err := w.Write(b); err != nil {
return err
}
return writePad(w, len(b))
}
func writePad(w io.Writer, n int) error {
pad := n % 8
if pad == 0 {
return nil
}
var zeroes [8]byte
_, err := w.Write(zeroes[:8-pad])
return err
}

387
cmd/netlogfmt/main.go Normal file
View File

@@ -0,0 +1,387 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// netlogfmt parses a stream of JSON log messages from stdin and
// formats the network traffic logs produced by "tailscale.com/wgengine/netlog"
// according to the schema in "tailscale.com/types/netlogtype.Message"
// in a more humanly readable format.
//
// Example usage:
//
// $ cat netlog.json | go run tailscale.com/cmd/netlogfmt
// =========================================================================================
// NodeID: n123456CNTRL
// Logged: 2022-10-13T20:23:10.165Z
// Window: 2022-10-13T20:23:09.644Z (5s)
// --------------------------------------------------- Tx[P/s] Tx[B/s] Rx[P/s] Rx[B/s]
// VirtualTraffic: 16.80 1.64Ki 11.20 1.03Ki
// TCP: 100.109.51.95:22 -> 100.85.80.41:42912 16.00 1.59Ki 10.40 1008.84
// TCP: 100.109.51.95:21291 -> 100.107.177.2:53133 0.40 27.60 0.40 24.20
// TCP: 100.109.51.95:21291 -> 100.107.177.2:53134 0.40 23.40 0.40 24.20
// PhysicalTraffic: 16.80 2.32Ki 11.20 1.48Ki
// 100.85.80.41 -> 192.168.0.101:41641 16.00 2.23Ki 10.40 1.40Ki
// 100.107.177.2 -> 192.168.0.100:41641 0.80 83.20 0.80 83.20
// =========================================================================================
package main
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"math"
"net/http"
"net/netip"
"os"
"strconv"
"strings"
"time"
"github.com/dsnet/try"
jsonv2 "github.com/go-json-experiment/json"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/logtail"
"tailscale.com/types/netlogtype"
"tailscale.com/util/must"
)
var (
resolveNames = flag.Bool("resolve-names", false, "convert tailscale IP addresses to hostnames; must also specify --api-key and --tailnet-id")
apiKey = flag.String("api-key", "", "API key to query the Tailscale API with; see https://login.tailscale.com/admin/settings/keys")
tailnetName = flag.String("tailnet-name", "", "tailnet domain name to lookup devices in; see https://login.tailscale.com/admin/settings/general")
)
var namesByAddr map[netip.Addr]string
func main() {
flag.Parse()
if *resolveNames {
namesByAddr = mustMakeNamesByAddr()
}
// The logic handles a stream of arbitrary JSON.
// So long as a JSON object seems like a network log message,
// then this will unmarshal and print it.
if err := processStream(os.Stdin); err != nil {
if err == io.EOF {
return
}
log.Fatalf("processStream: %v", err)
}
}
func processStream(r io.Reader) (err error) {
defer try.Handle(&err)
dec := jsonv2.NewDecoder(os.Stdin)
for {
processValue(dec)
}
}
func processValue(dec *jsonv2.Decoder) {
switch dec.PeekKind() {
case '[':
processArray(dec)
case '{':
processObject(dec)
default:
try.E(dec.SkipValue())
}
}
func processArray(dec *jsonv2.Decoder) {
try.E1(dec.ReadToken()) // parse '['
for dec.PeekKind() != ']' {
processValue(dec)
}
try.E1(dec.ReadToken()) // parse ']'
}
func processObject(dec *jsonv2.Decoder) {
var hasTraffic bool
var rawMsg []byte
try.E1(dec.ReadToken()) // parse '{'
for dec.PeekKind() != '}' {
// Capture any members that could belong to a network log message.
switch name := try.E1(dec.ReadToken()); name.String() {
case "virtualTraffic", "subnetTraffic", "exitTraffic", "physicalTraffic":
hasTraffic = true
fallthrough
case "logtail", "nodeId", "logged", "start", "end":
if len(rawMsg) == 0 {
rawMsg = append(rawMsg, '{')
} else {
rawMsg = append(rawMsg[:len(rawMsg)-1], ',')
}
rawMsg = append(append(append(rawMsg, '"'), name.String()...), '"')
rawMsg = append(rawMsg, ':')
rawMsg = append(rawMsg, try.E1(dec.ReadValue())...)
rawMsg = append(rawMsg, '}')
default:
processValue(dec)
}
}
try.E1(dec.ReadToken()) // parse '}'
// If this appears to be a network log message, then unmarshal and print it.
if hasTraffic {
var msg message
try.E(jsonv2.Unmarshal(rawMsg, &msg))
printMessage(msg)
}
}
type message struct {
Logtail struct {
ID logtail.PublicID `json:"id"`
Logged time.Time `json:"server_time"`
} `json:"logtail"`
Logged time.Time `json:"logged"`
netlogtype.Message
}
func printMessage(msg message) {
// Construct a table of network traffic per connection.
rows := [][7]string{{3: "Tx[P/s]", 4: "Tx[B/s]", 5: "Rx[P/s]", 6: "Rx[B/s]"}}
duration := msg.End.Sub(msg.Start)
addRows := func(heading string, traffic []netlogtype.ConnectionCounts) {
if len(traffic) == 0 {
return
}
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) bool {
nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes
ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes
return nx > ny
})
var sum netlogtype.Counts
for _, cc := range traffic {
sum = sum.Add(cc.Counts)
}
rows = append(rows, [7]string{
0: heading + ":",
3: formatSI(float64(sum.TxPackets) / duration.Seconds()),
4: formatIEC(float64(sum.TxBytes) / duration.Seconds()),
5: formatSI(float64(sum.RxPackets) / duration.Seconds()),
6: formatIEC(float64(sum.RxBytes) / duration.Seconds()),
})
if len(traffic) == 1 && traffic[0].Connection.IsZero() {
return // this is already a summary counts
}
formatAddrPort := func(a netip.AddrPort) string {
if !a.IsValid() {
return ""
}
if name, ok := namesByAddr[a.Addr()]; ok {
if a.Port() == 0 {
return name
}
return name + ":" + strconv.Itoa(int(a.Port()))
}
if a.Port() == 0 {
return a.Addr().String()
}
return a.String()
}
for _, cc := range traffic {
row := [7]string{
0: " ",
1: formatAddrPort(cc.Src),
2: formatAddrPort(cc.Dst),
3: formatSI(float64(cc.TxPackets) / duration.Seconds()),
4: formatIEC(float64(cc.TxBytes) / duration.Seconds()),
5: formatSI(float64(cc.RxPackets) / duration.Seconds()),
6: formatIEC(float64(cc.RxBytes) / duration.Seconds()),
}
if cc.Proto > 0 {
row[0] += cc.Proto.String() + ":"
}
rows = append(rows, row)
}
}
addRows("VirtualTraffic", msg.VirtualTraffic)
addRows("SubnetTraffic", msg.SubnetTraffic)
addRows("ExitTraffic", msg.ExitTraffic)
addRows("PhysicalTraffic", msg.PhysicalTraffic)
// Compute the maximum width of each field.
var maxWidths [7]int
for _, row := range rows {
for i, col := range row {
if maxWidths[i] < len(col) && !(i == 0 && !strings.HasPrefix(col, " ")) {
maxWidths[i] = len(col)
}
}
}
var maxSum int
for _, n := range maxWidths {
maxSum += n
}
// Output a table of network traffic per connection.
line := make([]byte, 0, maxSum+len(" ")+len(" -> ")+4*len(" "))
line = appendRepeatByte(line, '=', cap(line))
fmt.Println(string(line))
if !msg.Logtail.ID.IsZero() {
fmt.Printf("LogID: %s\n", msg.Logtail.ID)
}
if msg.NodeID != "" {
fmt.Printf("NodeID: %s\n", msg.NodeID)
}
formatTime := func(t time.Time) string {
return t.In(time.Local).Format("2006-01-02 15:04:05.000")
}
switch {
case !msg.Logged.IsZero():
fmt.Printf("Logged: %s\n", formatTime(msg.Logged))
case !msg.Logtail.Logged.IsZero():
fmt.Printf("Logged: %s\n", formatTime(msg.Logtail.Logged))
}
fmt.Printf("Window: %s (%0.3fs)\n", formatTime(msg.Start), duration.Seconds())
for i, row := range rows {
line = line[:0]
isHeading := !strings.HasPrefix(row[0], " ")
for j, col := range row {
if isHeading && j == 0 {
col = "" // headings will be printed later
}
switch j {
case 0, 2: // left justified
line = append(line, col...)
line = appendRepeatByte(line, ' ', maxWidths[j]-len(col))
case 1, 3, 4, 5, 6: // right justified
line = appendRepeatByte(line, ' ', maxWidths[j]-len(col))
line = append(line, col...)
}
switch j {
case 0:
line = append(line, " "...)
case 1:
if row[1] == "" && row[2] == "" {
line = append(line, " "...)
} else {
line = append(line, " -> "...)
}
case 2, 3, 4, 5:
line = append(line, " "...)
}
}
switch {
case i == 0: // print dashed-line table heading
line = appendRepeatByte(line[:0], '-', maxWidths[0]+len(" ")+maxWidths[1]+len(" -> ")+maxWidths[2])[:cap(line)]
case isHeading:
copy(line[:], row[0])
}
fmt.Println(string(line))
}
}
func mustMakeNamesByAddr() map[netip.Addr]string {
switch {
case *apiKey == "":
log.Fatalf("--api-key must be specified with --resolve-names")
case *tailnetName == "":
log.Fatalf("--tailnet must be specified with --resolve-names")
}
// Query the Tailscale API for a list of devices in the tailnet.
const apiURL = "https://api.tailscale.com/api/v2"
req := must.Get(http.NewRequest("GET", apiURL+"/tailnet/"+*tailnetName+"/devices", nil))
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(*apiKey+":")))
resp := must.Get(http.DefaultClient.Do(req))
defer resp.Body.Close()
b := must.Get(io.ReadAll(resp.Body))
if resp.StatusCode != 200 {
log.Fatalf("http: %v: %s", http.StatusText(resp.StatusCode), b)
}
// Unmarshal the API response.
var m struct {
Devices []struct {
Name string `json:"name"`
Addrs []netip.Addr `json:"addresses"`
} `json:"devices"`
}
must.Do(json.Unmarshal(b, &m))
// Construct a unique mapping of Tailscale IP addresses to hostnames.
// For brevity, we start with the first segment of the name and
// use more segments until we find the shortest prefix that is unique
// for all names in the tailnet.
seen := make(map[string]bool)
namesByAddr := make(map[netip.Addr]string)
retry:
for i := 0; i < 10; i++ {
maps.Clear(seen)
maps.Clear(namesByAddr)
for _, d := range m.Devices {
name := fieldPrefix(d.Name, i)
if seen[name] {
continue retry
}
seen[name] = true
for _, a := range d.Addrs {
namesByAddr[a] = name
}
}
return namesByAddr
}
panic("unable to produce unique mapping of address to names")
}
// fieldPrefix returns the first n number of dot-separated segments.
//
// Example:
//
// fieldPrefix("foo.bar.baz", 0) returns ""
// fieldPrefix("foo.bar.baz", 1) returns "foo"
// fieldPrefix("foo.bar.baz", 2) returns "foo.bar"
// fieldPrefix("foo.bar.baz", 3) returns "foo.bar.baz"
// fieldPrefix("foo.bar.baz", 4) returns "foo.bar.baz"
func fieldPrefix(s string, n int) string {
s0 := s
for i := 0; i < n && len(s) > 0; i++ {
if j := strings.IndexByte(s, '.'); j >= 0 {
s = s[j+1:]
} else {
s = ""
}
}
return strings.TrimSuffix(s0[:len(s0)-len(s)], ".")
}
func appendRepeatByte(b []byte, c byte, n int) []byte {
for i := 0; i < n; i++ {
b = append(b, c)
}
return b
}
func formatSI(n float64) string {
switch n := math.Abs(n); {
case n < 1e3:
return fmt.Sprintf("%0.2f ", n/(1e0))
case n < 1e6:
return fmt.Sprintf("%0.2fk", n/(1e3))
case n < 1e9:
return fmt.Sprintf("%0.2fM", n/(1e6))
default:
return fmt.Sprintf("%0.2fG", n/(1e9))
}
}
func formatIEC(n float64) string {
switch n := math.Abs(n); {
case n < 1<<10:
return fmt.Sprintf("%0.2f ", n/(1<<0))
case n < 1<<20:
return fmt.Sprintf("%0.2fKi", n/(1<<10))
case n < 1<<30:
return fmt.Sprintf("%0.2fMi", n/(1<<20))
default:
return fmt.Sprintf("%0.2fGi", n/(1<<30))
}
}

View File

@@ -1,5 +1,7 @@
# nginx-auth
[![status: experimental](https://img.shields.io/badge/status-experimental-blue)](https://tailscale.com/kb/1167/release-stages/#experimental)
This is a tool that allows users to use Tailscale Whois authentication with
NGINX as a reverse proxy. This allows users that already have a bunch of
services hosted on an internal NGINX server to point those domains to the

View File

@@ -4,7 +4,7 @@ set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.1
VERSION=0.1.2
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \

View File

@@ -75,12 +75,7 @@ func main() {
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
return
}
tailnet, _, ok = strings.Cut(tailnet, ".beta.tailscale.net")
if !ok {
w.WriteHeader(http.StatusUnauthorized)
log.Printf("can't extract tailnet name from hostname %q", info.Node.Name)
return
}
tailnet = strings.TrimSuffix(tailnet, ".beta.tailscale.net")
}
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {

42
cmd/pgproxy/README.md Normal file
View File

@@ -0,0 +1,42 @@
# pgproxy
The pgproxy server is a proxy for the Postgres wire protocol. [Read
more in our blog
post](https://tailscale.com/blog/introducing-pgproxy/) about it!
The proxy runs an in-process Tailscale instance, accepts postgres
client connections over Tailscale only, and proxies them to the
configured upstream postgres server.
This proxy exists because postgres clients default to very insecure
connection settings: either they "prefer" but do not require TLS; or
they set sslmode=require, which merely requires that a TLS handshake
took place, but don't verify the server's TLS certificate or the
presented TLS hostname. In other words, sslmode=require enforces that
a TLS session is created, but that session can trivially be
machine-in-the-middled to steal credentials, data, inject malicious
queries, and so forth.
Because this flaw is in the client's validation of the TLS session,
you have no way of reliably detecting the misconfiguration
server-side. You could fix the configuration of all the clients you
know of, but the default makes it very easy to accidentally regress.
Instead of trying to verify client configuration over time, this proxy
removes the need for postgres clients to be configured correctly: the
upstream database is configured to only accept connections from the
proxy, and the proxy is only available to clients over Tailscale.
Therefore, clients must use the proxy to connect to the database. The
client<>proxy connection is secured end-to-end by Tailscale, which the
proxy enforces by verifying that the connecting client is a known
current Tailscale peer. The proxy<>server connection is established by
the proxy itself, using strict TLS verification settings, and the
client is only allowed to communicate with the server once we've
established that the upstream connection is safe to use.
A couple side benefits: because clients can only connect via
Tailscale, you can use Tailscale ACLs as an extra layer of defense on
top of the postgres user/password authentication. And, the proxy can
maintain an audit log of who connected to the database, complete with
the strongly authenticated Tailscale identity of the client.

366
cmd/pgproxy/pgproxy.go Normal file
View File

@@ -0,0 +1,366 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The pgproxy server is a proxy for the Postgres wire protocol.
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"expvar"
"flag"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/metrics"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
"tailscale.com/types/logger"
)
var (
hostname = flag.String("hostname", "", "Tailscale hostname to serve on")
port = flag.Int("port", 5432, "Listening port for client connections")
debugPort = flag.Int("debug-port", 80, "Listening port for debug/metrics endpoint")
upstreamAddr = flag.String("upstream-addr", "", "Address of the upstream Postgres server, in host:port format")
upstreamCA = flag.String("upstream-ca-file", "", "File containing the PEM-encoded CA certificate for the upstream server")
tailscaleDir = flag.String("state-dir", "", "Directory in which to store the Tailscale auth state")
)
func main() {
flag.Parse()
if *hostname == "" {
log.Fatal("missing --hostname")
}
if *upstreamAddr == "" {
log.Fatal("missing --upstream-addr")
}
if *upstreamCA == "" {
log.Fatal("missing --upstream-ca-file")
}
if *tailscaleDir == "" {
log.Fatal("missing --state-dir")
}
ts := &tsnet.Server{
Dir: *tailscaleDir,
Hostname: *hostname,
// Make the stdout logs a clean audit log of connections.
Logf: logger.Discard,
}
if os.Getenv("TS_AUTHKEY") == "" {
log.Print("Note: you need to run this with TS_AUTHKEY=... the first time, to join your tailnet of choice.")
}
tsclient, err := ts.LocalClient()
if err != nil {
log.Fatalf("getting tsnet API client: %v", err)
}
p, err := newProxy(*upstreamAddr, *upstreamCA, tsclient)
if err != nil {
log.Fatal(err)
}
expvar.Publish("pgproxy", p.Expvar())
if *debugPort != 0 {
mux := http.NewServeMux()
tsweb.Debugger(mux)
srv := &http.Server{
Handler: mux,
}
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(srv.Serve(dln))
}()
}
ln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
log.Printf("serving access to %s on port %d", *upstreamAddr, *port)
log.Fatal(p.Serve(ln))
}
// proxy is a postgres wire protocol proxy, which strictly enforces
// the security of the TLS connection to its upstream regardless of
// what the client's TLS configuration is.
type proxy struct {
upstreamAddr string // "my.database.com:5432"
upstreamHost string // "my.database.com"
upstreamCertPool *x509.CertPool
downstreamCert []tls.Certificate
client *tailscale.LocalClient
activeSessions expvar.Int
startedSessions expvar.Int
errors metrics.LabelMap
}
// newProxy returns a proxy that forwards connections to
// upstreamAddr. The upstream's TLS session is verified using the CA
// cert(s) in upstreamCAPath.
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
bs, err := os.ReadFile(upstreamCAPath)
if err != nil {
return nil, err
}
upstreamCertPool := x509.NewCertPool()
if !upstreamCertPool.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("invalid CA cert in %q", upstreamCAPath)
}
h, _, err := net.SplitHostPort(upstreamAddr)
if err != nil {
return nil, err
}
downstreamCert, err := mkSelfSigned(h)
if err != nil {
return nil, err
}
return &proxy{
upstreamAddr: upstreamAddr,
upstreamHost: h,
upstreamCertPool: upstreamCertPool,
downstreamCert: []tls.Certificate{downstreamCert},
client: client,
errors: metrics.LabelMap{Label: "kind"},
}, nil
}
// Expvar returns p's monitoring metrics.
func (p *proxy) Expvar() expvar.Var {
ret := &metrics.Set{}
ret.Set("sessions_active", &p.activeSessions)
ret.Set("sessions_started", &p.startedSessions)
ret.Set("session_errors", &p.errors)
return ret
}
// Serve accepts postgres client connections on ln and proxies them to
// the configured upstream. ln can be any net.Listener, but all client
// connections must originate from tailscale IPs that can be verified
// with WhoIs.
func (p *proxy) Serve(ln net.Listener) error {
var lastSessionID int64
for {
c, err := ln.Accept()
if err != nil {
return err
}
id := time.Now().UnixNano()
if id == lastSessionID {
// Bluntly enforce SID uniqueness, even if collisions are
// fantastically unlikely (but OSes vary in how much timer
// precision they expose to the OS, so id might be rounded
// e.g. to the same millisecond)
id++
}
lastSessionID = id
go func(sessionID int64) {
if err := p.serve(sessionID, c); err != nil {
log.Printf("%d: session ended with error: %v", sessionID, err)
}
}(id)
}
}
var (
// sslStart is the magic bytes that postgres clients use to indicate
// that they want to do a TLS handshake. Servers should respond with
// the single byte "S" before starting a normal TLS handshake.
sslStart = [8]byte{0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f}
// plaintextStart is the magic bytes that postgres clients use to
// indicate that they're starting a plaintext authentication
// handshake.
plaintextStart = [8]byte{0, 0, 0, 86, 0, 3, 0, 0}
)
// serve proxies the postgres client on c to the proxy's upstream,
// enforcing strict TLS to the upstream.
func (p *proxy) serve(sessionID int64, c net.Conn) error {
defer c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
whois, err := p.client.WhoIs(ctx, c.RemoteAddr().String())
if err != nil {
p.errors.Add("whois-failed", 1)
return fmt.Errorf("getting client identity: %v", err)
}
// Before anything else, log the connection attempt.
user, machine := "", ""
if whois.Node != nil {
if whois.Node.Hostinfo.ShareeNode() {
machine = "external-device"
} else {
machine = strings.TrimSuffix(whois.Node.Name, ".")
}
}
if whois.UserProfile != nil {
user = whois.UserProfile.LoginName
if user == "tagged-devices" && whois.Node != nil {
user = strings.Join(whois.Node.Tags, ",")
}
}
if user == "" || machine == "" {
p.errors.Add("no-ts-identity", 1)
return fmt.Errorf("couldn't identify source user and machine (user %q, machine %q)", user, machine)
}
log.Printf("%d: session start, from %s (machine %s, user %s)", sessionID, c.RemoteAddr(), machine, user)
start := time.Now()
defer func() {
elapsed := time.Since(start)
log.Printf("%d: session end, from %s (machine %s, user %s), lasted %s", sessionID, c.RemoteAddr(), machine, user, elapsed.Round(time.Millisecond))
}()
// Read the client's opening message, to figure out if it's trying
// to TLS or not.
var buf [8]byte
if _, err := io.ReadFull(c, buf[:len(sslStart)]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("initial magic read: %v", err)
}
var clientIsTLS bool
switch {
case buf == sslStart:
clientIsTLS = true
case buf == plaintextStart:
clientIsTLS = false
default:
p.errors.Add("client-bad-protocol", 1)
return fmt.Errorf("unrecognized initial packet = % 02x", buf)
}
// Dial & verify upstream connection.
var d net.Dialer
d.Timeout = 10 * time.Second
upc, err := d.Dial("tcp", p.upstreamAddr)
if err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream dial: %v", err)
}
defer upc.Close()
if _, err := upc.Write(sslStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream write of start-ssl magic: %v", err)
}
if _, err := io.ReadFull(upc, buf[:1]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("reading upstream start-ssl response: %v", err)
}
if buf[0] != 'S' {
p.errors.Add("upstream-bad-protocol", 1)
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
}
tlsConf := &tls.Config{
ServerName: p.upstreamHost,
RootCAs: p.upstreamCertPool,
MinVersion: tls.VersionTLS12,
}
uptc := tls.Client(upc, tlsConf)
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("upstream-tls", 1)
return fmt.Errorf("upstream TLS handshake: %v", err)
}
// Accept the client conn and set it up the way the client wants.
var clientConn net.Conn
if clientIsTLS {
io.WriteString(c, "S") // yeah, we're good to speak TLS
s := tls.Server(c, &tls.Config{
ServerName: p.upstreamHost,
Certificates: p.downstreamCert,
MinVersion: tls.VersionTLS12,
})
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("client-tls", 1)
return fmt.Errorf("client TLS handshake: %v", err)
}
clientConn = s
} else {
// Repeat the header we read earlier up to the server.
if _, err := uptc.Write(plaintextStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("sending initial client bytes to upstream: %v", err)
}
clientConn = c
}
// Finally, proxy the client to the upstream.
errc := make(chan error, 1)
go func() {
_, err := io.Copy(uptc, clientConn)
errc <- err
}()
go func() {
_, err := io.Copy(clientConn, uptc)
errc <- err
}()
if err := <-errc; err != nil {
// Don't increment error counts here, because the most common
// cause of termination is client or server closing the
// connection normally, and it'll obscure "interesting"
// handshake errors.
return fmt.Errorf("session terminated with error: %v", err)
}
return nil
}
// mkSelfSigned creates and returns a self-signed TLS certificate for
// hostname.
func mkSelfSigned(hostname string) (tls.Certificate, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
if err != nil {
return tls.Certificate{}, err
}
pub := priv.Public()
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"pgproxy"},
},
DNSNames: []string{hostname},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(crand.Reader, &template, &template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: priv,
Leaf: cert,
}, nil
}

View File

@@ -19,6 +19,7 @@ import (
var (
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
goToolchainSRI = flag.Bool("go-sri", false, "print the SRI hash of the Tailscale Go toolchain")
alpine = flag.Bool("alpine", false, "print the tag of alpine docker image")
)
@@ -48,4 +49,7 @@ func main() {
}
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
}
if *goToolchainSRI {
fmt.Println(strings.TrimSpace(ts.GoToolchainSRI))
}
}

View File

@@ -110,11 +110,12 @@ func runSpeedtest(ctx context.Context, args []string) error {
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
fmt.Println("Results:")
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
startTime := results[0].IntervalStart
for _, r := range results {
if r.Total {
fmt.Fprintln(w, "-------------------------------------------------------------------------")
}
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
}
w.Flush()
return nil

View File

@@ -0,0 +1,189 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ssh-auth-none-demo is a demo SSH server that's meant to run on the
// public internet (at 188.166.70.128 port 2222) and
// highlight the unique parts of the Tailscale SSH server so SSH
// client authors can hit it easily and fix their SSH clients without
// needing to set up Tailscale and Tailscale SSH.
package main
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
var (
addr = flag.String("addr", ":2222", "address to listen on")
)
func main() {
flag.Parse()
cacheDir, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatal(err)
}
keys, err := getHostKeys(dir)
if err != nil {
log.Fatal(err)
}
if len(keys) == 0 {
log.Fatal("no host keys")
}
srv := &ssh.Server{
Addr: *addr,
Version: "Tailscale",
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
start := time.Now()
return &gossh.ServerConfig{
NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
return []string{"tailscale"}
},
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
totalBanners := 2
if cm.User() == "banners" {
totalBanners = 5
}
for banner := 2; banner <= totalBanners; banner++ {
time.Sleep(time.Second)
if banner == totalBanners {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
} else {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
}
}
return nil, nil
},
BannerCallback: func(cm gossh.ConnMetadata) string {
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
},
}
},
}
for _, signer := range keys {
srv.AddHostKey(signer)
}
log.Printf("Running on %s ...", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
log.Printf("done")
}
func handleSessionPostSSHAuth(s ssh.Session) {
log.Printf("Started session from user %q", s.User())
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
// Abort the session on Control-C or Control-D.
go func() {
buf := make([]byte, 1024)
for {
n, err := s.Read(buf)
for _, b := range buf[:n] {
if b <= 4 { // abort on Control-C (3) or Control-D (4)
io.WriteString(s, "bye\n")
s.Exit(1)
}
}
if err != nil {
return
}
}
}()
for i := 10; i > 0; i-- {
fmt.Fprintf(s, "%v ...\n", i)
time.Sleep(time.Second)
}
s.Exit(0)
}
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
for _, typ := range keyTypes {
hostKey, err := hostKeyFileOrCreate(dir, typ)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(hostKey)
if err != nil {
return nil, err
}
ret = append(ret, signer)
}
return ret, nil
}
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
if err == nil {
return v, nil
}
if !os.IsNotExist(err) {
return nil, err
}
var priv any
switch typ {
default:
return nil, fmt.Errorf("unsupported key type %q", typ)
case "ed25519":
_, priv, err = ed25519.GenerateKey(rand.Reader)
case "ecdsa":
// curve is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
curve := elliptic.P256()
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
case "rsa":
// keySize is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
const keySize = 2048
priv, err = rsa.GenerateKey(rand.Reader, keySize)
}
if err != nil {
return nil, err
}
mk, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
err = os.WriteFile(path, pemGen, 0700)
return pemGen, err
}

58
cmd/stunc/stunc.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Command stunc makes a STUN request to a STUN server and prints the result.
package main
import (
"log"
"net"
"os"
"tailscale.com/net/stun"
)
func main() {
log.SetFlags(0)
if len(os.Args) != 2 {
log.Fatalf("usage: %s <hostname>", os.Args[0])
}
host := os.Args[1]
uaddr, err := net.ResolveUDPAddr("udp", host+":3478")
if err != nil {
log.Fatal(err)
}
c, err := net.ListenUDP("udp", nil)
if err != nil {
log.Fatal(err)
}
txID := stun.NewTxID()
req := stun.Request(txID)
_, err = c.WriteToUDP(req, uaddr)
if err != nil {
log.Fatal(err)
}
var buf [1024]byte
n, raddr, err := c.ReadFromUDPAddrPort(buf[:])
if err != nil {
log.Fatal(err)
}
tid, saddr, err := stun.ParseResponse(buf[:n])
if err != nil {
log.Fatal(err)
}
if tid != txID {
log.Fatalf("txid mismatch: got %v, want %v", tid, txID)
}
log.Printf("sent addr: %v", uaddr)
log.Printf("from addr: %v", raddr)
log.Printf("stun addr: %v", saddr)
}

179
cmd/sync-containers/main.go Normal file
View File

@@ -0,0 +1,179 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The sync-containers command synchronizes container image tags from one
// registry to another.
//
// It is intended as a workaround for ghcr.io's lack of good push credentials:
// you can either authorize "classic" Personal Access Tokens in your org (which
// are a common vector of very bad compromise), or you can get a short-lived
// credential in a Github action.
//
// Since we publish to both Docker Hub and ghcr.io, we use this program in a
// Github action to effectively rsync from docker hub into ghcr.io, so that we
// can continue to forbid dangerous Personal Access Tokens in the tailscale org.
package main
import (
"context"
"flag"
"fmt"
"log"
"sort"
"strings"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types"
)
var (
src = flag.String("src", "", "Source image")
dst = flag.String("dst", "", "Destination image")
max = flag.Int("max", 0, "Maximum number of tags to sync (0 for all tags)")
dryRun = flag.Bool("dry-run", true, "Don't actually sync anything")
)
func main() {
flag.Parse()
if *src == "" {
log.Fatalf("--src is required")
}
if *dst == "" {
log.Fatalf("--dst is required")
}
opts := []remote.Option{
remote.WithAuthFromKeychain(authn.DefaultKeychain),
remote.WithContext(context.Background()),
}
stags, err := listTags(*src, opts...)
if err != nil {
log.Fatalf("listing source tags: %v", err)
}
dtags, err := listTags(*dst, opts...)
if err != nil {
log.Fatalf("listing destination tags: %v", err)
}
add, remove := diffTags(stags, dtags)
if l := len(add); l > 0 {
log.Printf("%d tags to push: %s", len(add), strings.Join(add, ", "))
if *max > 0 && l > *max {
log.Printf("Limiting sync to %d tags", *max)
add = add[:*max]
}
}
for _, tag := range add {
if !*dryRun {
log.Printf("Syncing tag %q", tag)
if err := copyTag(*src, *dst, tag, opts...); err != nil {
log.Printf("Syncing tag %q: progress error: %v", tag, err)
}
} else {
log.Printf("Dry run: would sync tag %q", tag)
}
}
if len(remove) > 0 {
log.Printf("%d tags to remove: %s\n", len(remove), strings.Join(remove, ", "))
log.Printf("Not removing any tags for safety.\n")
}
}
func copyTag(srcStr, dstStr, tag string, opts ...remote.Option) error {
src, err := name.ParseReference(fmt.Sprintf("%s:%s", srcStr, tag))
if err != nil {
return err
}
dst, err := name.ParseReference(fmt.Sprintf("%s:%s", dstStr, tag))
if err != nil {
return err
}
desc, err := remote.Get(src)
if err != nil {
return err
}
ch := make(chan v1.Update, 10)
opts = append(opts, remote.WithProgress(ch))
progressDone := make(chan struct{})
go func() {
defer close(progressDone)
for p := range ch {
fmt.Printf("Syncing tag %q: %d%% (%d/%d)\n", tag, int(float64(p.Complete)/float64(p.Total)*100), p.Complete, p.Total)
if p.Error != nil {
fmt.Printf("error: %v\n", p.Error)
}
}
}()
switch desc.MediaType {
case types.OCIManifestSchema1, types.DockerManifestSchema2:
img, err := desc.Image()
if err != nil {
return err
}
if err := remote.Write(dst, img, opts...); err != nil {
return err
}
case types.OCIImageIndex, types.DockerManifestList:
idx, err := desc.ImageIndex()
if err != nil {
return err
}
if err := remote.WriteIndex(dst, idx, opts...); err != nil {
return err
}
}
<-progressDone
return nil
}
func listTags(repoStr string, opts ...remote.Option) ([]string, error) {
repo, err := name.NewRepository(repoStr)
if err != nil {
return nil, err
}
tags, err := remote.List(repo, opts...)
if err != nil {
return nil, err
}
sort.Strings(tags)
return tags, nil
}
func diffTags(src, dst []string) (add, remove []string) {
srcd := make(map[string]bool)
for _, tag := range src {
srcd[tag] = true
}
dstd := make(map[string]bool)
for _, tag := range dst {
dstd[tag] = true
}
for _, tag := range src {
if !dstd[tag] {
add = append(add, tag)
}
}
for _, tag := range dst {
if !srcd[tag] {
remove = append(remove, tag)
}
}
sort.Strings(add)
sort.Strings(remove)
return add, remove
}

View File

@@ -7,8 +7,11 @@ package cli
import (
"context"
"errors"
"flag"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
@@ -16,6 +19,17 @@ var bugReportCmd = &ffcli.Command{
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
fs.BoolVar(&bugReportArgs.record, "record", false, "if true, pause and then write another bugreport")
return fs
})(),
}
var bugReportArgs struct {
diagnose bool
record bool
}
func runBugReport(ctx context.Context, args []string) error {
@@ -25,12 +39,46 @@ func runBugReport(ctx context.Context, args []string) error {
case 1:
note = args[0]
default:
return errors.New("unknown argumets")
return errors.New("unknown arguments")
}
logMarker, err := localClient.BugReport(ctx, note)
if err != nil {
return err
opts := tailscale.BugReportOpts{
Note: note,
Diagnose: bugReportArgs.diagnose,
}
outln(logMarker)
if !bugReportArgs.record {
// Simple, non-record case
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
if err != nil {
return err
}
outln(logMarker)
return nil
}
// Recording; run the request in the background
done := make(chan struct{})
opts.Record = done
type bugReportResp struct {
marker string
err error
}
resCh := make(chan bugReportResp, 1)
go func() {
m, err := localClient.BugReportWithOpts(ctx, opts)
resCh <- bugReportResp{m, err}
}()
outln("Recording started; please reproduce your issue and then press Enter...")
fmt.Scanln()
close(done)
res := <-resCh
if res.err != nil {
return res.err
}
outln(res.marker)
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
return nil
}

View File

@@ -7,7 +7,10 @@ package cli
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"log"
@@ -16,6 +19,7 @@ import (
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"software.sslmate.com/src/go-pkcs12"
"tailscale.com/atomicfile"
"tailscale.com/ipn"
"tailscale.com/version"
@@ -24,12 +28,12 @@ import (
var certCmd = &ffcli.Command{
Name: "cert",
Exec: runCert,
ShortHelp: "get TLS certs",
ShortHelp: "Get TLS certs",
ShortUsage: "cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert")
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
fs.StringVar(&certArgs.keyFile, "key-file", "", "output key file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
return fs
})(),
@@ -44,6 +48,7 @@ var certArgs struct {
func runCert(ctx context.Context, args []string) error {
if certArgs.serve {
s := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
GetCertificate: localClient.GetCertificate,
},
@@ -57,7 +62,16 @@ func runCert(ctx context.Context, args []string) error {
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
}),
}
log.Printf("running TLS server on :443 ...")
switch len(args) {
case 0:
// Nothing.
case 1:
s.Addr = args[0]
default:
return errors.New("too many arguments; max 1 allowed with --serve-demo (the listen address)")
}
log.Printf("running TLS server on %s ...", s.Addr)
return s.ListenAndServeTLS("", "")
}
@@ -119,17 +133,25 @@ func runCert(ctx context.Context, args []string) error {
}
}
}
if certArgs.keyFile != "" {
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
if dst := certArgs.keyFile; dst != "" {
contents := keyPEM
if isPKCS12(dst) {
var err error
contents, err = convertToPKCS12(certPEM, keyPEM)
if err != nil {
return err
}
}
keyChanged, err := writeIfChanged(dst, contents, 0600)
if err != nil {
return err
}
if certArgs.keyFile != "-" {
macWarn()
if keyChanged {
printf("Wrote private key to %v\n", certArgs.keyFile)
printf("Wrote private key to %v\n", dst)
} else {
printf("Private key unchanged at %v\n", certArgs.keyFile)
printf("Private key unchanged at %v\n", dst)
}
}
}
@@ -149,3 +171,29 @@ func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed
}
return true, nil
}
func isPKCS12(dst string) bool {
return strings.HasSuffix(dst, ".p12") || strings.HasSuffix(dst, ".pfx")
}
func convertToPKCS12(certPEM, keyPEM []byte) ([]byte, error) {
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
var certs []*x509.Certificate
for _, c := range cert.Certificate {
cert, err := x509.ParseCertificate(c)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
if len(certs) == 0 {
return nil, errors.New("no certs")
}
// TODO(bradfitz): I'm not sure this is right yet. The goal was to make this
// work for https://github.com/tailscale/tailscale/issues/2928 but I'm still
// fighting Windows.
return pkcs12.Encode(rand.Reader, cert.PrivateKey, certs[0], certs[1:], "" /* no password */)
}

View File

@@ -13,28 +13,28 @@ import (
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/version/distro"
)
var Stderr io.Writer = os.Stderr
var Stdout io.Writer = os.Stdout
func errf(format string, a ...any) {
fmt.Fprintf(Stderr, format, a...)
}
func printf(format string, a ...any) {
fmt.Fprintf(Stdout, format, a...)
}
@@ -142,7 +142,7 @@ func Run(args []string) (err error) {
})
rootfs := newFlagSet("tailscale")
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
rootCmd := &ffcli.Command{
Name: "tailscale",
@@ -157,7 +157,10 @@ change in the future.
Subcommands: []*ffcli.Command{
upCmd,
downCmd,
setCmd,
loginCmd,
logoutCmd,
switchCmd,
netcheckCmd,
ipCmd,
statusCmd,
@@ -177,15 +180,22 @@ change in the future.
UsageFunc: usageFunc,
}
for _, c := range rootCmd.Subcommands {
c.UsageFunc = usageFunc
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
rootCmd.Subcommands = append(rootCmd.Subcommands,
idTokenCmd,
)
}
// Don't advertise the debug command, but it exists.
if strSliceContains(args, "debug") {
// Don't advertise these commands, but they're still explicitly available.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "serve"):
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
@@ -231,68 +241,16 @@ var rootArgs struct {
socket string
}
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
c, err := safesocket.Connect(s)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
fatalf("--socket cannot be empty")
}
fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err)
}
clientToServer := func(b []byte) {
ipn.WriteMsg(c, b)
}
ctx, cancel := context.WithCancel(ctx)
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
select {
case <-interrupt:
case <-ctx.Done():
// Context canceled elsewhere.
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
return
}
c.Close()
cancel()
}()
bc := ipn.NewBackendClient(log.Printf, clientToServer)
return c, bc, ctx, cancel
}
// pump receives backend messages on conn and pushes them into bc.
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
defer conn.Close()
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(conn)
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return fmt.Errorf("%w (tailscaled stopped running?)", err)
}
return err
}
bc.GotNotifyMsg(msg)
}
return ctx.Err()
}
func strSliceContains(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
func usageFuncNoDefaultValues(c *ffcli.Command) string {
return usageFuncOpt(c, false)
}
func usageFunc(c *ffcli.Command) string {
return usageFuncOpt(c, true)
}
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
var b strings.Builder
fmt.Fprintf(&b, "USAGE\n")
@@ -323,6 +281,9 @@ func usageFunc(c *ffcli.Command) string {
c.FlagSet.VisitAll(func(f *flag.Flag) {
var s string
name, usage := flag.UnquoteUsage(f)
if strings.HasPrefix(usage, "HIDDEN: ") {
return
}
if isBoolFlag(f) {
s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
} else {
@@ -336,7 +297,14 @@ func usageFunc(c *ffcli.Command) string {
s += "\n \t"
s += strings.ReplaceAll(usage, "\n", "\n \t")
if f.DefValue != "" {
showDefault := f.DefValue != "" && withDefaults
// Issue 6766: don't show the default Windows socket path. It's long
// and distracting. And people on on Windows aren't likely to ever
// change it anyway.
if runtime.GOOS == "windows" && f.Name == "socket" && strings.HasPrefix(f.DefValue, `\\.\pipe\ProtectedPrefix\`) {
showDefault = false
}
if showDefault {
s += fmt.Sprintf(" (default %s)", f.DefValue)
}

View File

@@ -16,8 +16,10 @@ import (
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
@@ -36,10 +38,10 @@ var geese = []string{"linux", "darwin", "windows", "freebsd"}
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs := newUpFlagSet(goos, &upArgs, "up")
fs.VisitAll(func(f *flag.Flag) {
mp := new(ipn.MaskedPrefs)
updateMaskedPrefsFromUpFlag(mp, f.Name)
updateMaskedPrefsFromUpOrSetFlag(mp, f.Name)
got := mp.Pretty()
wantEmpty := preflessFlag(f.Name)
isEmpty := got == "MaskedPrefs{}"
@@ -410,7 +412,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
curPrefs: &ipn.Prefs{
@@ -448,7 +450,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
// might've had an old install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
@@ -478,6 +480,19 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
distro: "", // not Synology
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
},
{
name: "profile_name_ignored_in_up",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
NetfilterMode: preftype.NetfilterOn,
ProfileName: "foo",
},
goos: "linux",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -486,7 +501,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
goos = tt.goos
}
var upArgs upArgsT
flagSet := newUpFlagSet(goos, &upArgs)
flagSet := newUpFlagSet(goos, &upArgs, "up")
flags := CleanUpArgs(tt.flags)
flagSet.Parse(flags)
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
@@ -513,7 +528,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
}
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
fs := newUpFlagSet(goos, &args)
fs := newUpFlagSet(goos, &args, "up")
fs.Parse(flagArgs) // populates args
return
}
@@ -762,6 +777,9 @@ func TestPrefFlagMapping(t *testing.T) {
case "NotepadURLs":
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
continue
case "Egg":
// Not applicable.
continue
}
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
}
@@ -770,7 +788,7 @@ func TestPrefFlagMapping(t *testing.T) {
func TestFlagAppliesToOS(t *testing.T) {
for _, goos := range geese {
var upArgs upArgsT
fs := newUpFlagSet(goos, &upArgs)
fs := newUpFlagSet(goos, &upArgs, "up")
fs.VisitAll(func(f *flag.Flag) {
if !flagAppliesToOS(f.Name, goos) {
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
@@ -786,6 +804,10 @@ func TestUpdatePrefs(t *testing.T) {
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
// It is used to test the --accept-risks flag.
sshOverTailscale bool
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
// updatePrefs might've mutated them (from applyImplicitPrefs).
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
@@ -913,15 +935,159 @@ func TestUpdatePrefs(t *testing.T) {
}
},
},
{
name: "enable_ssh",
flags: []string{"--ssh"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh",
flags: []string{"--ssh=false"},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
runSSH: true,
}},
},
{
name: "disable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=false"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
RunSSH: true,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh_no_risk",
flags: []string{"--ssh=true"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "enable_ssh_over_ssh",
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if !newPrefs.RunSSH {
t.Errorf("RunSSH not set to true")
}
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "disable_ssh_over_ssh",
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
Persist: &persist.Persist{LoginName: "crawshaw.github"},
AllowSingleHosts: true,
CorpDNS: true,
RunSSH: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: &ipn.MaskedPrefs{
RunSSHSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
if newPrefs.RunSSH {
t.Errorf("RunSSH not set to false")
}
},
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.sshOverTailscale {
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
}
if tt.env.goos == "" {
tt.env.goos = "linux"
}
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs, "up")
flags := CleanUpArgs(tt.flags)
tt.env.flagSet.Parse(flags)
if err := tt.env.flagSet.Parse(flags); err != nil {
t.Fatal(err)
}
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
if err != nil {
@@ -936,6 +1102,8 @@ func TestUpdatePrefs(t *testing.T) {
return
}
t.Fatal(err)
} else if tt.wantErrSubtr != "" {
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
}
if tt.checkUpdatePrefsMutations != nil {
tt.checkUpdatePrefsMutations(t, newPrefs)
@@ -949,13 +1117,18 @@ func TestUpdatePrefs(t *testing.T) {
justEditMP.Prefs = ipn.Prefs{} // uninteresting
}
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
}
})
}
}
func asJSON(v any) string {
b, _ := json.MarshalIndent(v, "", "\t")
return string(b)
}
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
return a == b
})
@@ -985,3 +1158,81 @@ func TestCleanUpArgs(t *testing.T) {
c.Assert(got, qt.DeepEquals, tt.want)
}
}
func TestUpWorthWarning(t *testing.T) {
if !upWorthyWarning(healthmsg.WarnAcceptRoutesOff) {
t.Errorf("WarnAcceptRoutesOff of %q should be worth warning", healthmsg.WarnAcceptRoutesOff)
}
if !upWorthyWarning(healthmsg.TailscaleSSHOnBut + "some problem") {
t.Errorf("want true for SSH problems")
}
if upWorthyWarning("not in map poll") {
t.Errorf("want false for other misc errors")
}
}
func TestParseNLArgs(t *testing.T) {
tcs := []struct {
name string
input []string
parseKeys bool
parseDisablements bool
wantErr error
wantKeys []tka.Key
wantDisablements [][]byte
}{
{
name: "empty",
input: nil,
parseKeys: true,
parseDisablements: true,
},
{
name: "key no votes",
input: []string{"nlpub:" + strings.Repeat("00", 32)},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
},
{
name: "key with votes",
input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
},
{
name: "disablements",
input: []string{"disablement:" + strings.Repeat("02", 32), "disablement-secret:" + strings.Repeat("03", 32)},
parseDisablements: true,
wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
},
{
name: "disablements not allowed",
input: []string{"disablement:" + strings.Repeat("02", 32)},
parseKeys: true,
wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix nlpub:"),
},
{
name: "keys not allowed",
input: []string{"nlpub:" + strings.Repeat("02", 32)},
parseDisablements: true,
wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
keys, disablements, err := parseNLArgs(tc.input, tc.parseKeys, tc.parseDisablements)
if !reflect.DeepEqual(err, tc.wantErr) {
t.Fatalf("parseNLArgs(%v).err = %v, want %v", tc.input, err, tc.wantErr)
}
if !reflect.DeepEqual(keys, tc.wantKeys) {
t.Errorf("keys = %v, want %v", keys, tc.wantKeys)
}
if !reflect.DeepEqual(disablements, tc.wantDisablements) {
t.Errorf("disablements = %v, want %v", disablements, tc.wantDisablements)
}
})
}
}

View File

@@ -48,11 +48,11 @@ func runConfigureHost(ctx context.Context, args []string) error {
if uid := os.Getuid(); uid != 0 {
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
}
osVer := hostinfo.GetOSVersion()
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
hi := hostinfo.New()
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
if !isDSM6 && !isDSM7 {
return fmt.Errorf("unsupported DSM version %q", osVer)
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
}
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if err := os.MkdirAll("/dev/net", 0755); err != nil {

View File

@@ -17,7 +17,9 @@ import (
"log"
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"os"
"runtime"
"strconv"
@@ -25,14 +27,20 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/net/http/httpproxy"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlhttp"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tshttpproxy"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/must"
"tailscale.com/util/strs"
)
var debugCmd = &ffcli.Command{
@@ -42,7 +50,7 @@ var debugCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-seconds seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
return fs
@@ -53,6 +61,16 @@ var debugCmd = &ffcli.Command{
Exec: runDERPMap,
ShortHelp: "print DERP map",
},
{
Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
return fs
})(),
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
@@ -86,7 +104,7 @@ var debugCmd = &ffcli.Command{
{
Name: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale local API",
ShortHelp: "print how to access Tailscale LocalAPI",
},
{
Name: "restun",
@@ -115,6 +133,8 @@ var debugCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
fs.BoolVar(&watchIPNArgs.initial, "initial", false, "include initial status")
fs.BoolVar(&watchIPNArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
return fs
})(),
},
@@ -131,9 +151,25 @@ var debugCmd = &ffcli.Command{
fs := newFlagSet("ts2021")
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version")
fs.BoolVar(&ts2021Args.verbose, "verbose", false, "be extra verbose")
return fs
})(),
},
{
Name: "dev-store-set",
Exec: runDevStoreSet,
ShortHelp: "set a key/value pair during development",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("store-set")
fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger")
return fs
})(),
},
{
Name: "derp",
Exec: runDebugDERP,
ShortHelp: "test a DERP configuration",
},
},
}
@@ -168,9 +204,9 @@ func runDebug(ctx context.Context, args []string) error {
}
var usedFlag bool
if out := debugArgs.cpuFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
usedFlag = true // TODO(bradfitz): add "pprof" subcommand
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
if v, err := localClient.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
if v, err := localClient.Pprof(ctx, "profile", debugArgs.cpuSec); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@@ -180,9 +216,9 @@ func runDebug(ctx context.Context, args []string) error {
}
}
if out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
usedFlag = true // TODO(bradfitz): add "pprof" subcommand
log.Printf("Capturing memory profile ...")
if v, err := localClient.Profile(ctx, "heap", 0); err != nil {
if v, err := localClient.Pprof(ctx, "heap", 0); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@@ -203,9 +239,8 @@ func runDebug(ctx context.Context, args []string) error {
e.Encode(wfs)
return nil
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return localClient.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
if name, ok := strs.CutPrefix(debugArgs.file, "delete:"); ok {
return localClient.DeleteWaitingFile(ctx, name)
}
rc, size, err := localClient.GetWaitingFile(ctx, debugArgs.file)
if err != nil {
@@ -230,13 +265,42 @@ func runLocalCreds(ctx context.Context, args []string) error {
return nil
}
if runtime.GOOS == "windows" {
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
runLocalAPIProxy()
return nil
}
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
printf("curl --unix-socket %s http://local-tailscaled.sock/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
type localClientRoundTripper struct{}
func (localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return localClient.DoLocalRequest(req)
}
func runLocalAPIProxy() {
rp := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: apitype.LocalAPIHost,
Path: "/",
})
dir := rp.Director
rp.Director = func(req *http.Request) {
dir(req)
req.Host = ""
req.RequestURI = ""
}
rp.Transport = localClientRoundTripper{}
lc, err := net.Listen("tcp", "localhost:0")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Serving LocalAPI proxy on http://%s\n", lc.Addr())
fmt.Printf("curl.exe http://%v/localapi/v0/status\n", lc.Addr())
fmt.Printf("Ctrl+C to stop")
http.Serve(lc, rp)
}
var prefsArgs struct {
pretty bool
}
@@ -256,23 +320,36 @@ func runPrefs(ctx context.Context, args []string) error {
}
var watchIPNArgs struct {
netmap bool
netmap bool
initial bool
showPrivateKey bool
}
func runWatchIPN(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.SetNotifyCallback(func(n ipn.Notify) {
var mask ipn.NotifyWatchOpt
if watchIPNArgs.initial {
mask = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
}
if !watchIPNArgs.showPrivateKey {
mask |= ipn.NotifyNoPrivateKeys
}
watcher, err := localClient.WatchIPNBus(ctx, mask)
if err != nil {
return err
}
defer watcher.Close()
printf("Connected.\n")
for {
n, err := watcher.Next()
if err != nil {
return err
}
if !watchIPNArgs.netmap {
n.NetMap = nil
}
j, _ := json.MarshalIndent(n, "", "\t")
printf("%s\n", j)
})
bc.RequestEngineStatus()
pump(ctx, bc, c)
return errors.New("exit")
}
}
func runDERPMap(ctx context.Context, args []string) error {
@@ -445,19 +522,35 @@ func runVia(ctx context.Context, args []string) error {
var ts2021Args struct {
host string // "controlplane.tailscale.com"
version int // 27 or whatever
verbose bool
}
func runTS2021(ctx context.Context, args []string) error {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ltime | log.Lmicroseconds)
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
if ts2021Args.verbose {
u, err := url.Parse(keysURL)
if err != nil {
return err
}
envConf := httpproxy.FromEnvironment()
if *envConf == (httpproxy.Config{}) {
log.Printf("HTTP proxy env: (none)")
} else {
log.Printf("HTTP proxy env: %+v", envConf)
}
proxy, err := tshttpproxy.ProxyFromEnvironment(&http.Request{URL: u})
log.Printf("tshttpproxy.ProxyFromEnvironment = (%v, %v)", proxy, err)
}
machinePrivate := key.NewMachine()
var dialer net.Dialer
var keys struct {
PublicKey key.MachinePublic
}
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
log.Printf("Fetching keys from %s ...", keysURL)
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
if err != nil {
@@ -477,6 +570,9 @@ func runTS2021(ctx context.Context, args []string) error {
return fmt.Errorf("decoding /keys JSON: %w", err)
}
res.Body.Close()
if ts2021Args.verbose {
log.Printf("got public key: %v", keys.PublicKey)
}
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
log.Printf("Dial(%q, %q) ...", network, address)
@@ -488,8 +584,20 @@ func runTS2021(ctx context.Context, args []string) error {
}
return c, err
}
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
var logf logger.Logf
if ts2021Args.verbose {
logf = log.Printf
}
conn, err := (&controlhttp.Dialer{
Hostname: ts2021Args.host,
HTTPPort: "80",
HTTPSPort: "443",
MachineKey: machinePrivate,
ControlKey: keys.PublicKey,
ProtocolVersion: uint16(ts2021Args.version),
Dialer: dialFunc,
Logf: logf,
}).Dial(ctx)
log.Printf("controlhttp.Dial = %p, %v", conn, err)
if err != nil {
return err
@@ -505,3 +613,60 @@ func runTS2021(ctx context.Context, args []string) error {
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
return nil
}
var debugComponentLogsArgs struct {
forDur time.Duration
}
func runDebugComponentLogs(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug component-logs <component>")
}
component := args[0]
dur := debugComponentLogsArgs.forDur
err := localClient.SetComponentDebugLogging(ctx, component, dur)
if err != nil {
return err
}
if debugComponentLogsArgs.forDur <= 0 {
fmt.Printf("Disabled debug logs for component %q\n", component)
} else {
fmt.Printf("Enabled debug logs for component %q for %v\n", component, dur)
}
return nil
}
var devStoreSetArgs struct {
danger bool
}
func runDevStoreSet(ctx context.Context, args []string) error {
if len(args) != 2 {
return errors.New("usage: dev-store-set --danger <key> <value>")
}
if !devStoreSetArgs.danger {
return errors.New("this command is dangerous; use --danger to proceed")
}
key, val := args[0], args[1]
if val == "-" {
valb, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
val = string(valb)
}
return localClient.SetDevStoreKeyValue(ctx, key, val)
}
func runDebugDERP(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug derp <region>")
}
st, err := localClient.DebugDERPRegion(ctx, args[0])
if err != nil {
return err
}
fmt.Printf("%s\n", must.Get(json.MarshalIndent(st, "", " ")))
return nil
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build linux || windows || darwin
// +build linux windows darwin
package cli

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !linux && !windows && !darwin
// +build !linux,!windows,!darwin
package cli

View File

@@ -22,9 +22,13 @@ var downCmd = &ffcli.Command{
FlagSet: newDownFlagSet(),
}
var downArgs struct {
acceptedRisks string
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
registerAcceptRiskFlag(downf)
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
return downf
}
@@ -34,7 +38,7 @@ func runDown(ctx context.Context, args []string) error {
}
if isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
return err
}
}

View File

@@ -19,16 +19,20 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"unicode/utf8"
"github.com/mattn/go-isatty"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/time/rate"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/util/quarantine"
"tailscale.com/util/strs"
"tailscale.com/version"
)
@@ -48,6 +52,17 @@ var fileCmd = &ffcli.Command{
},
}
type countingReader struct {
io.Reader
n atomic.Uint64
}
func (c *countingReader) Read(buf []byte) (int, error) {
n, err := c.Reader.Read(buf)
c.n.Add(uint64(n))
return n, err
}
var fileCpCmd = &ffcli.Command{
Name: "cp",
ShortUsage: "file cp <files...> <target>:",
@@ -76,10 +91,10 @@ func runCp(ctx context.Context, args []string) error {
return errors.New("usage: tailscale file cp <files...> <target>:")
}
files, target := args[:len(args)-1], args[len(args)-1]
if !strings.HasSuffix(target, ":") {
target, ok := strs.CutSuffix(target, ":")
if !ok {
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
}
target = strings.TrimSuffix(target, ":")
hadBrackets := false
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
hadBrackets = true
@@ -115,11 +130,11 @@ func runCp(ctx context.Context, args []string) error {
}
for _, fileArg := range files {
var fileContents io.Reader
var fileContents *countingReader
var name = cpArgs.name
var contentLength int64 = -1
if fileArg == "-" {
fileContents = os.Stdin
fileContents = &countingReader{Reader: os.Stdin}
if name == "" {
name, fileContents, err = pickStdinFilename()
if err != nil {
@@ -143,19 +158,29 @@ func runCp(ctx context.Context, args []string) error {
return errors.New("directories not supported")
}
contentLength = fi.Size()
fileContents = io.LimitReader(f, contentLength)
fileContents = &countingReader{Reader: io.LimitReader(f, contentLength)}
if name == "" {
name = filepath.Base(fileArg)
}
if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
fileContents = &slowReader{r: fileContents}
fileContents = &countingReader{Reader: &slowReader{r: fileContents}}
}
}
if cpArgs.verbose {
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
var (
done = make(chan struct{}, 1)
wg sync.WaitGroup
)
if isatty.IsTerminal(os.Stderr.Fd()) {
go printProgress(&wg, done, fileContents, name, contentLength)
wg.Add(1)
}
err := localClient.PushFile(ctx, stableID, contentLength, name, fileContents)
if err != nil {
return err
@@ -163,10 +188,61 @@ func runCp(ctx context.Context, args []string) error {
if cpArgs.verbose {
log.Printf("sent %q", name)
}
done <- struct{}{}
wg.Wait()
}
return nil
}
const vtRestartLine = "\r\x1b[K"
func printProgress(wg *sync.WaitGroup, done <-chan struct{}, r *countingReader, name string, contentLength int64) {
defer wg.Done()
var lastBytesRead uint64
for {
select {
case <-done:
fmt.Fprintln(os.Stderr)
return
case <-time.After(time.Second):
n := r.n.Load()
contentLengthStr := "???"
if contentLength > 0 {
contentLengthStr = fmt.Sprint(contentLength / 1024)
}
fmt.Fprintf(os.Stderr, "%s%s\t\t%s", vtRestartLine, padTruncateString(name, 36), padTruncateString(fmt.Sprintf("%d/%s kb", n/1024, contentLengthStr), 16))
if contentLength > 0 {
fmt.Fprintf(os.Stderr, "\t%.02f%%", float64(n)/float64(contentLength)*100)
} else {
fmt.Fprintf(os.Stderr, "\t-------%%")
}
if lastBytesRead > 0 {
fmt.Fprintf(os.Stderr, "\t%d kb/s", (n-lastBytesRead)/1024)
} else {
fmt.Fprintf(os.Stderr, "\t-------")
}
lastBytesRead = n
}
}
}
func padTruncateString(str string, truncateAt int) string {
if len(str) <= truncateAt {
return str + strings.Repeat(" ", truncateAt-len(str))
}
// Truncate the string, but respect unicode codepoint boundaries.
// As of RFC3629 utf-8 codepoints can be at most 4 bytes wide.
for i := 1; i <= 4 && i < len(str)-truncateAt; i++ {
if utf8.ValidString(str[:truncateAt-i]) {
return str[:truncateAt-i] + "…"
}
}
return "" // Should be unreachable
}
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
ip, err := netip.ParseAddr(ipStr)
if err != nil {
@@ -229,12 +305,12 @@ func ext(b []byte) string {
// pickStdinFilename reads a bit of stdin to return a good filename
// for its contents. The returned Reader is the concatenation of the
// read and unread bits.
func pickStdinFilename() (name string, r io.Reader, err error) {
func pickStdinFilename() (name string, r *countingReader, err error) {
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
if err != nil {
return "", nil, err
}
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
return "stdin" + ext(sniff), &countingReader{Reader: io.MultiReader(bytes.NewReader(sniff), os.Stdin)}, nil
}
type slowReader struct {
@@ -393,6 +469,10 @@ func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targe
if err != nil {
return "", 0, err
}
// Apply quarantine attribute before copying
if err := quarantine.SetOnFile(f); err != nil {
return "", 0, fmt.Errorf("failed to apply quarantine attribute to file %v: %v", f.Name(), err)
}
_, err = io.Copy(f, rc)
if err != nil {
f.Close()
@@ -523,30 +603,16 @@ func wipeInbox(ctx context.Context) error {
}
func waitForFile(ctx context.Context) error {
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
fileWaiting := make(chan bool, 1)
notifyError := make(chan error, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
notifyError <- fmt.Errorf("Notify.ErrMessage: %v", *n.ErrMessage)
for {
ff, err := localClient.AwaitWaitingFiles(ctx, time.Hour)
if len(ff) > 0 {
return nil
}
if n.FilesWaiting != nil {
select {
case fileWaiting <- true:
default:
}
if err := ctx.Err(); err != nil {
return err
}
if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
return err
}
})
go pump(pumpCtx, bc, c)
select {
case <-fileWaiting:
return nil
case <-pumpCtx.Done():
return pumpCtx.Err()
case <-ctx.Done():
return ctx.Err()
case err := <-notifyError:
return err
}
}

View File

@@ -19,24 +19,27 @@ var licensesCmd = &ffcli.Command{
Exec: runLicenses,
}
func runLicenses(ctx context.Context, args []string) error {
var licenseURL string
// licensesURL returns the absolute URL containing open source license information for the current platform.
func licensesURL() string {
switch runtime.GOOS {
case "android":
licenseURL = "https://tailscale.com/licenses/android"
return "https://tailscale.com/licenses/android"
case "darwin", "ios":
licenseURL = "https://tailscale.com/licenses/apple"
return "https://tailscale.com/licenses/apple"
case "windows":
licenseURL = "https://tailscale.com/licenses/windows"
return "https://tailscale.com/licenses/windows"
default:
licenseURL = "https://tailscale.com/licenses/tailscale"
return "https://tailscale.com/licenses/tailscale"
}
}
func runLicenses(ctx context.Context, args []string) error {
licenses := licensesURL()
outln(`
Tailscale wouldn't be possible without the contributions of thousands of open
source developers. To see the open source packages included in Tailscale and
their respective license information, visit:
` + licenseURL)
` + licenses)
return nil
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"flag"
"github.com/peterbourgon/ff/v3/ffcli"
)
var loginArgs upArgsT
var loginCmd = &ffcli.Command{
Name: "login",
ShortUsage: "[ALPHA] login [flags]",
ShortHelp: "Log in to a Tailscale account",
LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
This command is currently in alpha and may change in the future.`,
UsageFunc: usageFunc,
FlagSet: func() *flag.FlagSet {
return newUpFlagSet(effectiveGOOS(), &loginArgs, "login")
}(),
Exec: func(ctx context.Context, args []string) error {
if err := localClient.SwitchToEmptyProfile(ctx); err != nil {
return err
}
return runUp(ctx, "login", args, loginArgs)
},
}

View File

@@ -10,7 +10,6 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"sort"
@@ -134,6 +133,9 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
printf("\t* HairPinning: %v\n", report.HairPinning)
printf("\t* PortMapping: %v\n", portMapping(report))
if report.CaptivePortal != "" {
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
}
// When DERP latency checking failed,
// magicsock will try to pick the DERP server that
@@ -202,7 +204,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}
defer res.Body.Close()
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
}

View File

@@ -5,30 +5,84 @@
package cli
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
)
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manipulate the tailnet key authority",
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
Exec: runNetworkLockStatus,
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manage tailnet lock",
LongHelp: "Manage tailnet lock",
Subcommands: []*ffcli.Command{
nlInitCmd,
nlStatusCmd,
nlAddCmd,
nlRemoveCmd,
nlSignCmd,
nlDisableCmd,
nlDisablementKDFCmd,
nlLogCmd,
nlLocalDisableCmd,
},
Exec: runNetworkLockStatus,
}
var nlInitArgs struct {
numDisablements int
disablementForSupport bool
confirm bool
}
var nlInitCmd = &ffcli.Command{
Name: "init",
ShortUsage: "init <public-key>...",
ShortHelp: "Initialize the tailnet key authority",
Exec: runNetworkLockInit,
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortHelp: "Initialize tailnet lock",
LongHelp: strings.TrimSpace(`
The 'tailscale lock init' command initializes tailnet lock for the
entire tailnet. The tailnet lock keys specified are those initially
trusted to sign nodes or to make further changes to tailnet lock.
You can identify the tailnet lock key for a node you wish to trust by
running 'tailscale lock' on that node, and copying the node's tailnet
lock key.
To disable tailnet lock, use the 'tailscale lock disable' command
along with one of the disablement secrets.
The number of disablement secrets to be generated is specified using the
--gen-disablements flag. Initializing tailnet lock requires at least
one disablement.
If --gen-disablement-for-support is specified, an additional disablement secret
will be generated and transmitted to Tailscale, which support can use to disable
tailnet lock. We recommend setting this flag.
`),
Exec: runNetworkLockInit,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock init")
fs.IntVar(&nlInitArgs.numDisablements, "gen-disablements", 1, "number of disablement secrets to generate")
fs.BoolVar(&nlInitArgs.disablementForSupport, "gen-disablement-for-support", false, "generates and transmits a disablement secret for Tailscale support")
fs.BoolVar(&nlInitArgs.confirm, "confirm", false, "do not prompt for confirmation")
return fs
})(),
}
func runNetworkLockInit(ctx context.Context, args []string) error {
@@ -37,48 +91,88 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
return fixTailscaledConnectError(err)
}
if st.Enabled {
return errors.New("network-lock is already enabled")
return errors.New("tailnet lock is already enabled")
}
// Parse the set of initially-trusted keys.
// Keys are specified using their key.NLPublic.MarshalText representation,
// with an optional '?<votes>' suffix.
var keys []tka.Key
for i, a := range args {
var key key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
return fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: key.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
status, err := localClient.NetworkLockInit(ctx, keys)
// Parse initially-trusted keys & disablement values.
keys, disablementValues, err := parseNLArgs(args, true, true)
if err != nil {
return err
}
fmt.Printf("Status: %+v\n\n", status)
// Common mistake: Not specifying the current node's key as one of the trusted keys.
foundSelfKey := false
for _, k := range keys {
keyID, err := k.ID()
if err != nil {
return err
}
if bytes.Equal(keyID, st.PublicKey.KeyID()) {
foundSelfKey = true
break
}
}
if !foundSelfKey {
return errors.New("the tailnet lock key of the current node must be one of the trusted keys during initialization")
}
fmt.Println("You are initializing tailnet lock with the following trusted signing keys:")
for _, k := range keys {
fmt.Printf(" - tlpub:%x (%s key)\n", k.Public, k.Kind.String())
}
fmt.Println()
if !nlInitArgs.confirm {
fmt.Printf("%d disablement secrets will be generated.\n", nlInitArgs.numDisablements)
if nlInitArgs.disablementForSupport {
fmt.Println("A disablement secret will be generated and transmitted to Tailscale support.")
}
genSupportFlag := ""
if nlInitArgs.disablementForSupport {
genSupportFlag = "--gen-disablement-for-support "
}
fmt.Println("\nIf this is correct, please re-run this command with the --confirm flag:")
fmt.Printf("\t%s lock init --confirm --gen-disablements %d %s%s", os.Args[0], nlInitArgs.numDisablements, genSupportFlag, strings.Join(args, " "))
fmt.Println()
return nil
}
fmt.Printf("%d disablement secrets have been generated and are printed below. Take note of them now, they WILL NOT be shown again.\n", nlInitArgs.numDisablements)
for i := 0; i < nlInitArgs.numDisablements; i++ {
var secret [32]byte
if _, err := rand.Read(secret[:]); err != nil {
return err
}
fmt.Printf("\tdisablement-secret:%X\n", secret[:])
disablementValues = append(disablementValues, tka.DisablementKDF(secret[:]))
}
var supportDisablement []byte
if nlInitArgs.disablementForSupport {
supportDisablement = make([]byte, 32)
if _, err := rand.Read(supportDisablement); err != nil {
return err
}
disablementValues = append(disablementValues, tka.DisablementKDF(supportDisablement))
fmt.Println("A disablement secret for Tailscale support has been generated and will be transmitted to Tailscale upon initialization.")
}
// The state returned by NetworkLockInit likely doesn't contain the initialized state,
// because that has to tick through from netmaps.
if _, err := localClient.NetworkLockInit(ctx, keys, disablementValues, supportDisablement); err != nil {
return err
}
fmt.Println("Initialization complete.")
return nil
}
var nlStatusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status",
ShortHelp: "Outputs the state of network lock",
ShortHelp: "Outputs the state of tailnet lock",
LongHelp: "Outputs the state of tailnet lock",
Exec: runNetworkLockStatus,
}
@@ -88,14 +182,356 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
return fixTailscaledConnectError(err)
}
if st.Enabled {
fmt.Println("Network-lock is ENABLED.")
fmt.Println("Tailnet lock is ENABLED.")
} else {
fmt.Println("Network-lock is NOT enabled.")
fmt.Println("Tailnet lock is NOT enabled.")
}
p, err := st.PublicKey.MarshalText()
fmt.Println()
if st.Enabled && st.NodeKey != nil && !st.PublicKey.IsZero() {
if st.NodeKeySigned {
fmt.Println("This node is accessible under tailnet lock.")
} else {
fmt.Println("This node is LOCKED OUT by tailnet-lock, and action is required to establish connectivity.")
fmt.Printf("Run the following command on a node with a trusted key:\n\ttailscale lock sign %v %s\n", st.NodeKey, st.PublicKey.CLIString())
}
fmt.Println()
}
if !st.PublicKey.IsZero() {
fmt.Printf("This node's tailnet-lock key: %s\n", st.PublicKey.CLIString())
fmt.Println()
}
if st.Enabled && len(st.TrustedKeys) > 0 {
fmt.Println("Trusted signing keys:")
for _, k := range st.TrustedKeys {
var line strings.Builder
line.WriteString("\t")
line.WriteString(k.Key.CLIString())
line.WriteString("\t")
line.WriteString(fmt.Sprint(k.Votes))
line.WriteString("\t")
if k.Key == st.PublicKey {
line.WriteString("(self)")
}
fmt.Println(line.String())
}
}
if st.Enabled && len(st.FilteredPeers) > 0 {
fmt.Println()
fmt.Println("The following nodes are locked out by tailnet lock and cannot connect to other nodes:")
for _, p := range st.FilteredPeers {
var line strings.Builder
line.WriteString("\t")
line.WriteString(p.Name)
line.WriteString("\t")
for i, addr := range p.TailscaleIPs {
line.WriteString(addr.String())
if i < len(p.TailscaleIPs)-1 {
line.WriteString(", ")
}
}
line.WriteString("\t")
line.WriteString(string(p.StableID))
fmt.Println(line.String())
}
}
return nil
}
var nlAddCmd = &ffcli.Command{
Name: "add",
ShortUsage: "add <public-key>...",
ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
LongHelp: "Adds one or more trusted signing keys to tailnet lock",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, args, nil)
},
}
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, nil, args)
},
}
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
// values/secrets.
// The keys encoded in args should be specified using their key.NLPublic.MarshalText
// representation with an optional '?<votes>' suffix.
// Disablement values or secrets must be encoded in hex with a prefix of 'disablement:' or
// 'disablement-secret:'.
//
// If any element could not be parsed,
// a nil slice is returned along with an appropriate error.
func parseNLArgs(args []string, parseKeys, parseDisablements bool) (keys []tka.Key, disablements [][]byte, err error) {
for i, a := range args {
if parseDisablements && (strings.HasPrefix(a, "disablement:") || strings.HasPrefix(a, "disablement-secret:")) {
b, err := hex.DecodeString(a[strings.Index(a, ":")+1:])
if err != nil {
return nil, nil, fmt.Errorf("parsing disablement %d: %v", i+1, err)
}
disablements = append(disablements, b)
continue
}
if !parseKeys {
return nil, nil, fmt.Errorf("parsing argument %d: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", i+1, a)
}
var nlpk key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
return nil, nil, fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: nlpk.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return nil, nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
return keys, disablements, nil
}
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
if !st.Enabled {
return errors.New("tailnet lock is not enabled")
}
addKeys, _, err := parseNLArgs(addArgs, true, false)
if err != nil {
return err
}
fmt.Printf("our public-key: %s\n", p)
removeKeys, _, err := parseNLArgs(removeArgs, true, false)
if err != nil {
return err
}
if err := localClient.NetworkLockModify(ctx, addKeys, removeKeys); err != nil {
return err
}
return nil
}
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>]",
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
LongHelp: "Signs a node key and transmits the signature to the coordination server",
Exec: runNetworkLockSign,
}
func runNetworkLockSign(ctx context.Context, args []string) error {
var (
nodeKey key.NodePublic
rotationKey key.NLPublic
)
if len(args) == 0 || len(args) > 2 {
return errors.New("usage: lock sign <node-key> [<rotation-key>]")
}
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
return fmt.Errorf("decoding node-key: %w", err)
}
if len(args) > 1 {
if err := rotationKey.UnmarshalText([]byte(args[1])); err != nil {
return fmt.Errorf("decoding rotation-key: %w", err)
}
}
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
}
var nlDisableCmd = &ffcli.Command{
Name: "disable",
ShortUsage: "disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
LongHelp: strings.TrimSpace(`
The 'tailscale lock disable' command uses the specified disablement
secret to disable tailnet lock.
If tailnet lock is re-enabled, new disablement secrets can be generated.
Once this secret is used, it has been distributed
to all nodes in the tailnet and should be considered public.
`),
Exec: runNetworkLockDisable,
}
func runNetworkLockDisable(ctx context.Context, args []string) error {
_, secrets, err := parseNLArgs(args, false, true)
if err != nil {
return err
}
if len(secrets) != 1 {
return errors.New("usage: lock disable <disablement-secret>")
}
return localClient.NetworkLockDisable(ctx, secrets[0])
}
var nlLocalDisableCmd = &ffcli.Command{
Name: "local-disable",
ShortUsage: "local-disable",
ShortHelp: "Disables tailnet lock for this node only",
LongHelp: strings.TrimSpace(`
The 'tailscale lock local-disable' command disables tailnet lock for only
the current node.
If the current node is locked out, this does not mean that it can initiate
connections in a tailnet with tailnet lock enabled. Rather, this means
that the current node will accept traffic from other nodes in the tailnet
that are locked out.
`),
Exec: runNetworkLockLocalDisable,
}
func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
return localClient.NetworkLockForceLocalDisable(ctx)
}
var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
Exec: runNetworkLockDisablementKDF,
}
func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock disablement-kdf <hex-encoded-disablement-secret>")
}
secret, err := hex.DecodeString(args[0])
if err != nil {
return err
}
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
return nil
}
var nlLogArgs struct {
limit int
}
var nlLogCmd = &ffcli.Command{
Name: "log",
ShortUsage: "log [--limit N]",
ShortHelp: "List changes applied to tailnet lock",
LongHelp: "List changes applied to tailnet lock",
Exec: runNetworkLockLog,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock log")
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
return fs
})(),
}
func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, error) {
terminalYellow := ""
terminalClear := ""
if color {
terminalYellow = "\x1b[33m"
terminalClear = "\x1b[0m"
}
var stanza strings.Builder
printKey := func(key *tka.Key, prefix string) {
fmt.Fprintf(&stanza, "%sType: %s\n", prefix, key.Kind.String())
if keyID, err := key.ID(); err == nil {
fmt.Fprintf(&stanza, "%sKeyID: %x\n", prefix, keyID)
} else {
// Older versions of the client shouldn't explode when they encounter an
// unknown key type.
fmt.Fprintf(&stanza, "%sKeyID: <Error: %v>\n", prefix, err)
}
if key.Meta != nil {
fmt.Fprintf(&stanza, "%sMetadata: %+v\n", prefix, key.Meta)
}
}
var aum tka.AUM
if err := aum.Unserialize(update.Raw); err != nil {
return "", fmt.Errorf("decoding: %w", err)
}
fmt.Fprintf(&stanza, "%supdate %x (%s)%s\n", terminalYellow, update.Hash, update.Change, terminalClear)
switch update.Change {
case tka.AUMAddKey.String():
printKey(aum.Key, "")
case tka.AUMRemoveKey.String():
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
case tka.AUMUpdateKey.String():
fmt.Fprintf(&stanza, "KeyID: %x\n", aum.KeyID)
if aum.Votes != nil {
fmt.Fprintf(&stanza, "Votes: %d\n", aum.Votes)
}
if aum.Meta != nil {
fmt.Fprintf(&stanza, "Metadata: %+v\n", aum.Meta)
}
case tka.AUMCheckpoint.String():
fmt.Fprintln(&stanza, "Disablement values:")
for _, v := range aum.State.DisablementSecrets {
fmt.Fprintf(&stanza, " - %x\n", v)
}
fmt.Fprintln(&stanza, "Keys:")
for _, k := range aum.State.Keys {
printKey(&k, " ")
}
default:
// Print a JSON encoding of the AUM as a fallback.
e := json.NewEncoder(&stanza)
e.SetIndent("", "\t")
if err := e.Encode(aum); err != nil {
return "", err
}
stanza.WriteRune('\n')
}
return stanza.String(), nil
}
func runNetworkLockLog(ctx context.Context, args []string) error {
updates, err := localClient.NetworkLockLog(ctx, nlLogArgs.limit)
if err != nil {
return fixTailscaledConnectError(err)
}
useColor := isatty.IsTerminal(os.Stdout.Fd())
stdOut := colorable.NewColorableStdout()
for _, update := range updates {
stanza, err := nlDescribeUpdate(update, useColor)
if err != nil {
return err
}
fmt.Fprintln(stdOut, stanza)
}
return nil
}

View File

@@ -16,9 +16,9 @@ import (
)
var (
riskTypes []string
acceptedRisks string
riskLoseSSH = registerRiskType("lose-ssh")
riskTypes []string
riskLoseSSH = registerRiskType("lose-ssh")
riskAll = registerRiskType("all")
)
func registerRiskType(riskType string) string {
@@ -28,14 +28,15 @@ func registerRiskType(riskType string) string {
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
// in presentRiskToUser.
func registerAcceptRiskFlag(f *flag.FlagSet) {
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
}
// riskAccepted reports whether riskType is in acceptedRisks.
func riskAccepted(riskType string) bool {
// isRiskAccepted reports whether riskType is in the comma-separated list of
// risks in acceptedRisks.
func isRiskAccepted(riskType, acceptedRisks string) bool {
for _, r := range strings.Split(acceptedRisks, ",") {
if r == riskType {
if r == riskType || r == riskAll {
return true
}
}
@@ -49,12 +50,16 @@ var errAborted = errors.New("aborted, no changes made")
// It is used by the presentRiskToUser function below.
const riskAbortTimeSeconds = 5
// presentRiskToUser displays the risk message and waits for the user to
// cancel. It returns errorAborted if the user aborts.
func presentRiskToUser(riskType, riskMessage string) error {
if riskAccepted(riskType) {
// presentRiskToUser displays the risk message and waits for the user to cancel.
// It returns errorAborted if the user aborts. In tests it returns errAborted
// immediately unless the risk has been explicitly accepted.
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
if isRiskAccepted(riskType, acceptedRisks) {
return nil
}
if inTest() {
return errAborted
}
outln(riskMessage)
printf("To skip this warning, use --accept-risk=%s\n", riskType)

729
cmd/tailscale/cli/serve.go Normal file
View File

@@ -0,0 +1,729 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/slices"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
var serveCmd = newServeCommand(&serveEnv{})
// newServeCommand returns a new "serve" subcommand using e as its environmment.
func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "[ALPHA] Serve from your Tailscale node",
ShortUsage: strings.TrimSpace(`
serve [flags] <mount-point> {proxy|path|text} <arg>
serve [flags] <sub-command> [sub-flags] <args>`),
LongHelp: strings.TrimSpace(`
*** ALPHA; all of this is subject to change ***
The 'tailscale serve' set of commands allows you to serve
content and local servers from your Tailscale node to
your tailnet.
You can also choose to enable the Tailscale Funnel with:
'tailscale serve funnel on'. Funnel allows you to publish
a 'tailscale serve' server publicly, open to the entire
internet. See https://tailscale.com/funnel.
EXAMPLES
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale serve / proxy 3000
- To serve a single file or a directory of files:
$ tailscale serve / path /home/alice/blog/index.html
$ tailscale serve /images/ path /home/alice/blog/images
- To serve simple static text:
$ tailscale serve / text "Hello, world!"
`),
Exec: e.runServe,
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
}),
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "tcp",
Exec: e.runServeTCP,
ShortHelp: "add or remove a TCP port forward",
LongHelp: strings.Join([]string{
"EXAMPLES",
" - Forward TLS over TCP to a local TCP server on port 5432:",
" $ tailscale serve tcp 5432",
"",
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
" $ tailscale serve --terminate-tls tcp 5432",
}, "\n"),
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
}),
UsageFunc: usageFunc,
},
{
Name: "funnel",
Exec: e.runServeFunnel,
ShortUsage: "funnel [flags] {on|off}",
ShortHelp: "turn Tailscale Funnel on or off",
LongHelp: strings.Join([]string{
"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.",
}, "\n"),
UsageFunc: usageFunc,
},
},
}
}
func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.FlagSet {
onError, out := flag.ExitOnError, Stderr
if e.testFlagOut != nil {
onError, out = flag.ContinueOnError, e.testFlagOut
}
fs := flag.NewFlagSet(name, onError)
fs.SetOutput(out)
if setup != nil {
setup(fs)
}
return fs
}
// serveEnv is the environment the serve command runs within. All I/O should be
// done via serveEnv methods so that it can be faked out for tests.
//
// It also contains the flags, as registered with newServeCommand.
type serveEnv struct {
// flags
servePort uint // Port to serve on. Defaults to 443.
terminateTLS bool
remove bool // remove a serve config
json bool // output JSON (status only for now)
// optional stuff for tests:
testFlagOut io.Writer
testGetServeConfig func(context.Context) (*ipn.ServeConfig, error)
testSetServeConfig func(context.Context, *ipn.ServeConfig) error
testGetLocalClientStatus func(context.Context) (*ipnstate.Status, error)
testStdout io.Writer
}
// getSelfDNSName returns the DNS name of the current node.
// The trailing dot is removed.
// Returns an error if local client status fails.
func (e *serveEnv) getSelfDNSName(ctx context.Context) (string, error) {
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return "", fmt.Errorf("getting client status: %w", err)
}
return strings.TrimSuffix(st.Self.DNSName, "."), nil
}
// getLocalClientStatus calls LocalClient.Status, checks if
// Status is ready.
// Returns error if unable to reach tailscaled or if self node is nil.
// Exits if status is not running or starting.
func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status, error) {
if e.testGetLocalClientStatus != nil {
return e.testGetLocalClientStatus(ctx)
}
st, err := localClient.Status(ctx)
if err != nil {
return nil, fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
fmt.Fprintf(os.Stderr, "%s\n", description)
os.Exit(1)
}
if st.Self == nil {
return nil, errors.New("no self node")
}
return st, nil
}
func (e *serveEnv) getServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
if e.testGetServeConfig != nil {
return e.testGetServeConfig(ctx)
}
return localClient.GetServeConfig(ctx)
}
func (e *serveEnv) setServeConfig(ctx context.Context, c *ipn.ServeConfig) error {
if e.testSetServeConfig != nil {
return e.testSetServeConfig(ctx, c)
}
return localClient.SetServeConfig(ctx, c)
}
// validateServePort returns --serve-port flag value,
// or an error if the port is not a valid port to serve on.
func (e *serveEnv) validateServePort() (port uint16, err error) {
// make sure e.servePort is uint16
port = uint16(e.servePort)
if uint(port) != e.servePort {
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
}
// make sure e.servePort is 443, 8443 or 10000
if port != 443 && port != 8443 && port != 10000 {
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
}
return port, nil
}
// runServe is the entry point for the "serve" subcommand, managing Web
// serve config types like proxy, path, and text.
//
// Examples:
// - tailscale serve / proxy 3000
// - tailscale serve /images/ path /var/www/images/
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if len(args) == 0 {
return flag.ErrHelp
}
// Undocumented debug command (not using ffcli subcommands) to set raw
// configs from stdin for now (2022-11-13).
if len(args) == 1 && args[0] == "set-raw" {
valb, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
sc := new(ipn.ServeConfig)
if err := json.Unmarshal(valb, sc); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
return localClient.SetServeConfig(ctx, sc)
}
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
mount, err := cleanMountPoint(args[0])
if err != nil {
return err
}
if e.remove {
return e.handleWebServeRemove(ctx, mount)
}
h := new(ipn.HTTPHandler)
switch args[1] {
case "path":
if version.IsSandboxedMacOS() {
// don't allow path serving for now on macOS (2022-11-15)
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
}
if !filepath.IsAbs(args[2]) {
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
return flag.ErrHelp
}
fi, err := os.Stat(args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
return flag.ErrHelp
}
if fi.IsDir() && !strings.HasSuffix(mount, "/") {
// dir mount points must end in /
// for relative file links to work
mount += "/"
}
h.Path = args[2]
case "proxy":
t, err := expandProxyTarget(args[2])
if err != nil {
return err
}
h.Proxy = t
case "text":
if args[2] == "" {
return errors.New("unable to serve; text cannot be an empty string")
}
h.Text = args[2]
default:
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
return flag.ErrHelp
}
cursc, err := e.getServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
if sc.IsTCPForwardingOnPort(srvPort) {
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
return flag.ErrHelp
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
}
mak.Set(&sc.Web[hp].Handlers, mount, h)
for k, v := range sc.Web[hp].Handlers {
if v == h {
continue
}
// If the new mount point ends in / and another mount point
// shares the same prefix, remove the other handler.
// (e.g. /foo/ overwrites /foo)
// The opposite example is also handled.
m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/")
if m1 == m2 {
delete(sc.Web[hp].Handlers, k)
continue
}
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.setServeConfig(ctx, sc); err != nil {
return err
}
}
return nil
}
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
sc, err := e.getServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
return errors.New("error: serve config does not exist")
}
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if sc.IsTCPForwardingOnPort(srvPort) {
return errors.New("cannot remove web handler; currently serving TCP")
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
if !sc.WebHandlerExists(hp, mount) {
return errors.New("error: serve config does not exist")
}
// delete existing handler, then cascade delete if empty
delete(sc.Web[hp].Handlers, mount)
if len(sc.Web[hp].Handlers) == 0 {
delete(sc.Web, hp)
delete(sc.TCP, srvPort)
}
// clear empty maps mostly for testing
if len(sc.Web) == 0 {
sc.Web = nil
}
if len(sc.TCP) == 0 {
sc.TCP = nil
}
if err := e.setServeConfig(ctx, sc); err != nil {
return err
}
return nil
}
func cleanMountPoint(mount string) (string, error) {
if mount == "" {
return "", errors.New("mount point cannot be empty")
}
if !strings.HasPrefix(mount, "/") {
mount = "/" + mount
}
c := path.Clean(mount)
if mount == c || mount == c+"/" {
return mount, nil
}
return "", fmt.Errorf("invalid mount point %q", mount)
}
func expandProxyTarget(target string) (string, error) {
if allNumeric(target) {
p, err := strconv.ParseUint(target, 10, 16)
if p == 0 || err != nil {
return "", fmt.Errorf("invalid port %q", target)
}
return "http://127.0.0.1:" + target, nil
}
if !strings.Contains(target, "://") {
target = "http://" + target
}
u, err := url.ParseRequestURI(target)
if err != nil {
return "", fmt.Errorf("parsing url: %w", err)
}
switch u.Scheme {
case "http", "https", "https+insecure":
// ok
default:
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
}
host := u.Hostname()
switch host {
// TODO(shayne,bradfitz): do we want to do this?
case "localhost", "127.0.0.1":
host = "127.0.0.1"
default:
return "", fmt.Errorf("only localhost or 127.0.0.1 proxies are currently supported")
}
url := u.Scheme + "://" + host
if u.Port() != "" {
url += ":" + u.Port()
}
return url, nil
}
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return s != ""
}
// runServeStatus prints the current serve config.
//
// Examples:
// - tailscale status
// - tailscale status --json
func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
sc, err := e.getServeConfig(ctx)
if err != nil {
return err
}
if e.json {
j, err := json.MarshalIndent(sc, "", " ")
if err != nil {
return err
}
j = append(j, '\n')
e.stdout().Write(j)
return nil
}
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
printf("No serve config\n")
return nil
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return err
}
if sc.IsTCPForwardingAny() {
if err := printTCPStatusTree(ctx, sc, st); err != nil {
return err
}
printf("\n")
}
for hp := range sc.Web {
printWebStatusTree(sc, hp)
printf("\n")
}
// warn when funnel on without handlers
for hp, a := range sc.AllowFunnel {
if !a {
continue
}
_, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
}
}
return nil
}
func (e *serveEnv) stdout() io.Writer {
if e.testStdout != nil {
return e.testStdout
}
return os.Stdout
}
func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error {
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
for p, h := range sc.TCP {
if h.TCPForward == "" {
continue
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(p))))
tlsStatus := "TLS over TCP"
if h.TerminateTLS != "" {
tlsStatus = "TLS terminated"
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
printf("|-- tcp://%s (%s, %s)\n", hp, tlsStatus, fStatus)
for _, a := range st.TailscaleIPs {
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(p)))
printf("|-- tcp://%s\n", ipp)
}
printf("|--> tcp://%s\n", h.TCPForward)
}
return nil
}
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
if sc == nil {
return
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
host, portStr, _ := net.SplitHostPort(string(hp))
if portStr == "443" {
printf("https://%s (%s)\n", host, fStatus)
} else {
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
}
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
switch {
case h.Path != "":
return "path", h.Path
case h.Proxy != "":
return "proxy", h.Proxy
case h.Text != "":
return "text", "\"" + elipticallyTruncate(h.Text, 20) + "\""
}
return "", ""
}
var mounts []string
for k := range sc.Web[hp].Handlers {
mounts = append(mounts, k)
}
sort.Slice(mounts, func(i, j int) bool {
return len(mounts[i]) < len(mounts[j])
})
maxLen := len(mounts[len(mounts)-1])
for _, m := range mounts {
h := sc.Web[hp].Handlers[m]
t, d := srvTypeAndDesc(h)
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
}
}
func elipticallyTruncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-3] + "..."
}
// runServeTCP is the entry point for the "serve tcp" subcommand and
// manages the serve config for TCP forwarding.
//
// Examples:
// - tailscale serve tcp 5432
// - tailscale --serve-port=8443 tcp 4430
// - tailscale --serve-port=10000 --terminate-tls tcp 8080
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
portStr := args[0]
p, err := strconv.ParseUint(portStr, 10, 16)
if p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
}
cursc, err := e.getServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
fwdAddr := "127.0.0.1:" + portStr
if sc.IsServingWeb(srvPort) {
if e.remove {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
}
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
}
if e.remove {
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
delete(sc.TCP, srvPort)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
return e.setServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if e.terminateTLS {
sc.TCP[srvPort].TerminateTLS = dnsName
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.setServeConfig(ctx, sc); err != nil {
return err
}
}
return nil
}
// runServeFunnel is the entry point for the "serve funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
var on bool
switch args[0] {
case "on", "off":
on = args[0] == "on"
default:
return flag.ErrHelp
}
sc, err := e.getServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
if err := checkHasAccess(st.Self.Capabilities); err != nil {
return err
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
if on == sc.AllowFunnel[hp] {
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
if err := e.setServeConfig(ctx, sc); err != nil {
return err
}
return nil
}
// checkHasAccess checks three things: 1) an invite was used to join the
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
// If any of these are false, an error is returned describing the problem.
//
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for Funnel.
func checkHasAccess(nodeAttrs []string) error {
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
}
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
return nil
}

View File

@@ -0,0 +1,700 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
func TestCleanMountPoint(t *testing.T) {
tests := []struct {
mount string
want string
wantErr bool
}{
{"foo", "/foo", false}, // missing prefix
{"/foo/", "/foo/", false}, // keep trailing slash
{"////foo", "", true}, // too many slashes
{"/foo//", "", true}, // too many slashes
{"", "", true}, // empty
{"https://tailscale.com", "", true}, // not a path
}
for _, tt := range tests {
mp, err := cleanMountPoint(tt.mount)
if err != nil && tt.wantErr {
continue
}
if err != nil {
t.Fatal(err)
}
if mp != tt.want {
t.Fatalf("got %q, want %q", mp, tt.want)
}
}
}
func TestCheckHasAccess(t *testing.T) {
tests := []struct {
caps []string
wantErr bool
}{
{[]string{}, true}, // No "funnel" attribute
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
{[]string{tailcfg.NodeAttrFunnel}, false},
}
for _, tt := range tests {
err := checkHasAccess(tt.caps)
switch {
case err != nil && tt.wantErr,
err == nil && !tt.wantErr:
continue
case tt.wantErr:
t.Fatalf("got no error, want error")
case !tt.wantErr:
t.Fatalf("got error %v, want no error", err)
}
}
}
func TestServeConfigMutations(t *testing.T) {
// Stateful mutations, starting from an empty config.
type step struct {
command []string // serve args; nil means no command to run (only reset)
reset bool // if true, reset all ServeConfig state
want *ipn.ServeConfig // non-nil means we want a save of this value
wantErr func(error) (badErrMsg string) // nil means no error is wanted
line int // line number of addStep call, for error messages
}
var steps []step
add := func(s step) {
_, _, s.line, _ = runtime.Caller(1)
steps = append(steps, s)
}
// funnel
add(step{reset: true})
add(step{
command: cmd("funnel on"),
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
})
add(step{
command: cmd("funnel on"),
want: nil, // nothing to save
})
add(step{
command: cmd("funnel off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("funnel off"),
want: nil, // nothing to save
})
add(step{
command: cmd("funnel"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
// https
add(step{reset: true})
add(step{
command: cmd("/ proxy 0"), // invalid port, too low
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy 65536"), // invalid port, too high
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy somehost"), // invalid host
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy http://otherhost"), // invalid host
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("--serve-port=9999 /abc proxy 3001"),
wantErr: anyErr(),
}) // invalid port
add(step{
command: cmd("--serve-port=8443 /abc proxy 3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("--serve-port=10000 / text hi"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Text: "hi"},
}},
},
},
})
add(step{
command: cmd("--remove /foo"),
want: nil, // nothing to save
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
add(step{
command: cmd("--remove --serve-port=10000 /"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("--remove /"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("--remove --serve-port=8443 /abc"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("bar proxy https://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "https://127.0.0.1:8443"},
}},
},
},
})
add(step{
command: cmd("bar proxy https://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{reset: true})
add(step{
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "https+insecure://127.0.0.1:3001"},
}},
},
},
})
add(step{reset: true})
add(step{
command: cmd("/foo proxy localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // test a second handler on the same port
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/foo": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
// tcp
add(step{reset: true})
add(step{
command: cmd("tcp 5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:5432"},
},
},
})
add(step{
command: cmd("tcp -terminate-tls 8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:8443",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{
command: cmd("tcp -terminate-tls 8443"),
want: nil, // nothing to save
})
add(step{
command: cmd("tcp --terminate-tls 8444"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:8444",
TerminateTLS: "foo.test.ts.net",
},
},
},
})
add(step{
command: cmd("tcp -terminate-tls=false 8445"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:8445"},
},
},
})
add(step{reset: true})
add(step{
command: cmd("tcp 123"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:123"},
},
},
})
add(step{
command: cmd("--remove tcp 321"),
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
add(step{
command: cmd("--remove tcp 123"),
want: &ipn.ServeConfig{},
})
// text
add(step{reset: true})
add(step{
command: cmd("/ text hello"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Text: "hello"},
}},
},
},
})
// path
td := t.TempDir()
writeFile := func(suffix, contents string) {
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
t.Fatal(err)
}
}
add(step{reset: true})
writeFile("foo", "this is foo")
add(step{
command: cmd("/ path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "foo")},
}},
},
},
})
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
writeFile("subdir/file-a", "this is A")
add(step{
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "foo")},
"/some/where": {Path: filepath.Join(td, "subdir/file-a")},
}},
},
},
})
add(step{
command: cmd("/ path missing"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{
command: cmd("/ path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
})
add(step{
command: cmd("--remove /"),
want: &ipn.ServeConfig{},
})
// combos
add(step{reset: true})
add(step{
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("funnel on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // serving on secondary port doesn't change funnel
command: cmd("--serve-port=8443 /bar proxy 3001"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{ // turn funnel on for secondary port
command: cmd("--serve-port=8443 funnel on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{ // turn funnel off for primary port 443
command: cmd("funnel off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
"/bar": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{ // remove secondary port
command: cmd("--serve-port=8443 --remove /bar"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // start a tcp forwarder on 8443
command: cmd("--serve-port=8443 tcp 5432"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // remove primary port http handler
command: cmd("--remove /"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
},
})
add(step{ // remove tcp forwarder
command: cmd("--serve-port=8443 --remove tcp 5432"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
},
})
add(step{ // turn off funnel
command: cmd("--serve-port=8443 funnel off"),
want: &ipn.ServeConfig{},
})
// tricky steps
add(step{reset: true})
add(step{ // a directory with a trailing slash mount point
command: cmd("/dir path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
})
add(step{ // this should overwrite the previous one
command: cmd("/dir path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir": {Path: filepath.Join(td, "foo")},
}},
},
},
})
add(step{reset: true}) // reset and do the opposite
add(step{ // a file without a trailing slash mount point
command: cmd("/dir path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir": {Path: filepath.Join(td, "foo")},
}},
},
},
})
add(step{ // this should overwrite the previous one
command: cmd("/dir path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/dir/": {Path: filepath.Join(td, "subdir/")},
}},
},
},
})
// error states
add(step{reset: true})
add(step{ // make sure we can't add "tcp" as if it was a mount
command: cmd("tcp text foo"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // "/tcp" is fine though as a mount
command: cmd("/tcp text foo"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/tcp": {Text: "foo"},
}},
},
},
})
add(step{reset: true})
add(step{ // tcp forward 5432 on serve port 443
command: cmd("tcp 5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "127.0.0.1:5432"},
},
},
})
add(step{ // try to start a web handler on the same port
command: cmd("/ proxy 3000"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{ // start a web handler on port 443
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
command: cmd("tcp 5432"),
wantErr: anyErr(),
})
// And now run the steps above.
var current *ipn.ServeConfig
for i, st := range steps {
if st.reset {
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
current = nil
}
if st.command == nil {
continue
}
t.Logf("Executing step #%d, line %v: %q ... ", i, st.line, st.command)
var stdout bytes.Buffer
var flagOut bytes.Buffer
var newState *ipn.ServeConfig
e := &serveEnv{
testFlagOut: &flagOut,
testStdout: &stdout,
testGetLocalClientStatus: func(context.Context) (*ipnstate.Status, error) {
return &ipnstate.Status{
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel},
},
}, nil
},
testGetServeConfig: func(context.Context) (*ipn.ServeConfig, error) {
return current, nil
},
testSetServeConfig: func(_ context.Context, c *ipn.ServeConfig) error {
newState = c
return nil
},
}
cmd := newServeCommand(e)
err := cmd.ParseAndRun(context.Background(), st.command)
if flagOut.Len() > 0 {
t.Logf("flag package output: %q", flagOut.Bytes())
}
if err != nil {
if st.wantErr == nil {
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, err)
}
if bad := st.wantErr(err); bad != "" {
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, bad)
}
continue
}
if st.wantErr != nil {
t.Fatalf("step #%d, line %v: got success (saved=%v), but wanted an error", i, st.line, newState != nil)
}
if !reflect.DeepEqual(newState, st.want) {
t.Fatalf("[%d] %v: bad state. got:\n%s\n\nwant:\n%s\n",
i, st.command, asJSON(newState), asJSON(st.want))
// NOTE: asJSON will omit empty fields, which might make
// result in bad state got/want diffs being the same, even
// though the actual state is different. Use below to debug:
// t.Fatalf("[%d] %v: bad state. got:\n%+v\n\nwant:\n%+v\n",
// i, st.command, newState, st.want)
}
if newState != nil {
current = newState
}
}
}
// exactError returns an error checker that wants exactly the provided want error.
// If optName is non-empty, it's used in the error message.
func exactErr(want error, optName ...string) func(error) string {
return func(got error) string {
if got == want {
return ""
}
if len(optName) > 0 {
return fmt.Sprintf("got error %v, want %v", got, optName[0])
}
return fmt.Sprintf("got error %v, want %v", got, want)
}
}
// anyErr returns an error checker that wants any error.
func anyErr() func(error) string {
return func(got error) string {
return ""
}
}
func cmd(s string) []string {
cmds := strings.Fields(s)
fmt.Printf("cmd: %v", cmds)
return cmds
}

183
cmd/tailscale/cli/set.go Normal file
View File

@@ -0,0 +1,183 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"net/netip"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/safesocket"
)
var setCmd = &ffcli.Command{
Name: "set",
ShortUsage: "set [flags]",
ShortHelp: "Change specified preferences",
LongHelp: `"tailscale set" allows changing specific preferences.
Unlike "tailscale up", this command does not require the complete set of desired settings.
Only settings explicitly mentioned will be set. There are no default values.`,
FlagSet: setFlagSet,
Exec: runSet,
UsageFunc: usageFuncNoDefaultValues,
}
type setArgsT struct {
acceptRoutes bool
acceptDNS bool
exitNodeIP string
exitNodeAllowLANAccess bool
shieldsUp bool
runSSH bool
hostname string
advertiseRoutes string
advertiseDefaultRoute bool
opUser string
acceptedRisks string
profileName string
forceDaemon bool
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf := newFlagSet("set")
setf.StringVar(&setArgs.profileName, "nickname", "", "nickname for the current account")
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
setf.BoolVar(&setArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
setf.StringVar(&setArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
setf.StringVar(&setArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
switch goos {
case "windows":
setf.BoolVar(&setArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
registerAcceptRiskFlag(setf, &setArgs.acceptedRisks)
return setf
}
var (
setArgs setArgsT
setFlagSet = newSetFlagSet(effectiveGOOS(), &setArgs)
)
func runSet(ctx context.Context, args []string) (retErr error) {
if len(args) > 0 {
fatalf("too many non-flag arguments: %q", args)
}
st, err := localClient.Status(ctx)
if err != nil {
return err
}
maskedPrefs := &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
ProfileName: setArgs.profileName,
RouteAll: setArgs.acceptRoutes,
CorpDNS: setArgs.acceptDNS,
ExitNodeAllowLANAccess: setArgs.exitNodeAllowLANAccess,
ShieldsUp: setArgs.shieldsUp,
RunSSH: setArgs.runSSH,
Hostname: setArgs.hostname,
OperatorUser: setArgs.opUser,
ForceDaemon: setArgs.forceDaemon,
},
}
if setArgs.exitNodeIP != "" {
if err := maskedPrefs.Prefs.SetExitNodeIP(setArgs.exitNodeIP, st); err != nil {
var e ipn.ExitNodeLocalIPError
if errors.As(err, &e) {
return fmt.Errorf("%w; did you mean --advertise-exit-node?", err)
}
return err
}
}
var advertiseExitNodeSet, advertiseRoutesSet bool
setFlagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name)
switch f.Name {
case "advertise-exit-node":
advertiseExitNodeSet = true
case "advertise-routes":
advertiseRoutesSet = true
}
})
if maskedPrefs.IsEmpty() {
return flag.ErrHelp
}
curPrefs, err := localClient.GetPrefs(ctx)
if err != nil {
return err
}
if maskedPrefs.AdvertiseRoutesSet {
maskedPrefs.AdvertiseRoutes, err = calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet, curPrefs, setArgs)
if err != nil {
return err
}
}
if maskedPrefs.RunSSHSet {
wantSSH, haveSSH := maskedPrefs.RunSSH, curPrefs.RunSSH
if err := presentSSHToggleRisk(wantSSH, haveSSH, setArgs.acceptedRisks); err != nil {
return err
}
}
checkPrefs := curPrefs.Clone()
checkPrefs.ApplyEdits(maskedPrefs)
if err := localClient.CheckPrefs(ctx, checkPrefs); err != nil {
return err
}
_, err = localClient.EditPrefs(ctx, maskedPrefs)
return err
}
// calcAdvertiseRoutesForSet returns the new value for Prefs.AdvertiseRoutes based on the
// current value, the flags passed to "tailscale set".
// advertiseExitNodeSet is whether the --advertise-exit-node flag was set.
// advertiseRoutesSet is whether the --advertise-routes flag was set.
// curPrefs is the current Prefs.
// setArgs is the parsed command-line arguments.
func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, curPrefs *ipn.Prefs, setArgs setArgsT) (routes []netip.Prefix, err error) {
if advertiseExitNodeSet && advertiseRoutesSet {
return calcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute)
}
if advertiseRoutesSet {
return calcAdvertiseRoutes(setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode())
}
if advertiseExitNodeSet {
alreadyAdvertisesExitNode := curPrefs.AdvertisesExitNode()
if alreadyAdvertisesExitNode == setArgs.advertiseDefaultRoute {
return curPrefs.AdvertiseRoutes, nil
}
routes = tsaddr.FilterPrefixesCopy(curPrefs.AdvertiseRoutes, func(p netip.Prefix) bool {
return p.Bits() != 0
})
if setArgs.advertiseDefaultRoute {
routes = append(routes, tsaddr.AllIPv4(), tsaddr.AllIPv6())
}
return routes, nil
}
return nil, nil
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"net/netip"
"reflect"
"testing"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/types/ptr"
)
func TestCalcAdvertiseRoutesForSet(t *testing.T) {
pfx := netip.MustParsePrefix
tests := []struct {
name string
setExit *bool
setRoutes *string
was []netip.Prefix
want []netip.Prefix
}{
{
name: "empty",
},
{
name: "advertise-exit",
setExit: ptr.To(true),
want: tsaddr.ExitRoutes(),
},
{
name: "advertise-exit/already-routes",
was: []netip.Prefix{pfx("34.0.0.0/16")},
setExit: ptr.To(true),
want: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
},
{
name: "advertise-exit/already-exit",
was: tsaddr.ExitRoutes(),
setExit: ptr.To(true),
want: tsaddr.ExitRoutes(),
},
{
name: "stop-advertise-exit",
was: tsaddr.ExitRoutes(),
setExit: ptr.To(false),
want: nil,
},
{
name: "stop-advertise-exit/with-routes",
was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
setExit: ptr.To(false),
want: []netip.Prefix{pfx("34.0.0.0/16")},
},
{
name: "advertise-routes",
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
},
{
name: "advertise-routes/already-exit",
was: tsaddr.ExitRoutes(),
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
},
{
name: "advertise-routes/already-diff-routes",
was: []netip.Prefix{pfx("34.0.0.0/16")},
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
},
{
name: "stop-advertise-routes",
was: []netip.Prefix{pfx("34.0.0.0/16")},
setRoutes: ptr.To(""),
want: nil,
},
{
name: "stop-advertise-routes/already-exit",
was: []netip.Prefix{pfx("34.0.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
setRoutes: ptr.To(""),
want: tsaddr.ExitRoutes(),
},
{
name: "advertise-routes-and-exit",
setExit: ptr.To(true),
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
},
{
name: "advertise-routes-and-exit/already-exit",
was: tsaddr.ExitRoutes(),
setExit: ptr.To(true),
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
},
{
name: "advertise-routes-and-exit/already-routes",
was: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16")},
setExit: ptr.To(true),
setRoutes: ptr.To("10.0.0.0/24,192.168.0.0/16"),
want: []netip.Prefix{pfx("10.0.0.0/24"), pfx("192.168.0.0/16"), tsaddr.AllIPv4(), tsaddr.AllIPv6()},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
curPrefs := &ipn.Prefs{
AdvertiseRoutes: tc.was,
}
sa := setArgsT{}
if tc.setExit != nil {
sa.advertiseDefaultRoute = *tc.setExit
}
if tc.setRoutes != nil {
sa.advertiseRoutes = *tc.setRoutes
}
got, err := calcAdvertiseRoutesForSet(tc.setExit != nil, tc.setRoutes != nil, curPrefs, sa)
if err != nil {
t.Fatal(err)
}
tsaddr.SortPrefixes(got)
tsaddr.SortPrefixes(tc.want)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}

View File

@@ -21,6 +21,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/version"
)
@@ -28,7 +29,23 @@ var sshCmd = &ffcli.Command{
Name: "ssh",
ShortUsage: "ssh [user@]<host> [args...]",
ShortHelp: "SSH to a Tailscale machine",
Exec: runSSH,
LongHelp: strings.TrimSpace(`
The 'tailscale ssh' command is an optional wrapper around the system 'ssh'
command that's useful in some cases. Tailscale SSH does not require its use;
most users running the Tailscale SSH server will prefer to just use the normal
'ssh' command or their normal SSH client.
The 'tailscale ssh' wrapper adds a few things:
* It resolves the destination server name in its arguments using MagicDNS,
even if --accept-dns=false.
* It works in userspace-networking mode, by supplying a ProxyCommand to the
system 'ssh' command that connects via a pipe through tailscaled.
* It automatically checks the destination server's SSH host key against the
node's SSH host key as advertised via the Tailscale coordination server.
`),
Exec: runSSH,
}
func runSSH(ctx context.Context, args []string) error {
@@ -94,10 +111,15 @@ func runSSH(ctx context.Context, args []string) error {
// So don't use it for now. MagicDNS is usually working on macOS anyway
// and they're not in userspace mode, so 'nc' isn't very useful.
if runtime.GOOS != "darwin" {
socketArg := ""
if rootArgs.socket != "" && rootArgs.socket != paths.DefaultTailscaledSocket() {
socketArg = fmt.Sprintf("--socket=%q", rootArgs.socket)
}
argv = append(argv,
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
"-o", fmt.Sprintf("ProxyCommand %q %s nc %%h %%p",
tailscaleBin,
rootArgs.socket,
socketArg,
))
}

View File

@@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build !js && !windows
// +build !js,!windows
package cli

Some files were not shown because too many files have changed in this diff Show More