Compare commits

...

273 Commits

Author SHA1 Message Date
Denton Gentry
9fc6551b4e VERSION.txt: this is v1.26.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-06 15:59:36 -07:00
Brad Fitzpatrick
db83926121 go.toolchain.rev: bump Go to 1.18.3 (+ Tailscale patches)
See 04d67b90d8

Diff:

bb6009ec7c..04d67b90d8

Change-Id: Ic0abd3058f3696c3f8007e1004ab4bf377c5323c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-06 15:46:28 -07:00
Mihai Parparita
27a1ad6a70 wasm: exclude code that's not used on iOS for Wasm too
It has similar size constraints. Saves ~1.9MB from the Wasm build.

Updates #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-06 13:52:52 -07:00
Brad Fitzpatrick
4007601f73 cmd/controlclient: wire up PingRequest peerapi pings too
Updates tailscale/corp#754

Change-Id: I61ac3fc44783b54bd02455bcb0baf19159b7a9d2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-06 13:41:34 -07:00
Maisem Ali
3b55bf9306 build_docker.sh: add run.sh as an entrypoint to the docker image
Fixes #4071

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-07 00:45:49 +05:00
Maisem Ali
bf2fa7b184 go.mod: pin github.com/tailscale/mkctr (try #2)
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-07 00:44:21 +05:00
Brad Fitzpatrick
0d972678e7 cmd/tailscale/cli: disable 'tailscale ssh' on sandboxed macOS
Updates #3802
Updates #4518
Fixes #4628

Change-Id: I194d2cc30fc8e38b66d4910787efbce14317b0ff
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-06 08:54:38 -07:00
Brad Fitzpatrick
0df3b76c25 tsweb: fix Port80Handler redirect to https with FQDN unset
Fixes the current http://pkgs.tailscale.com/ redirect to https:///
as that server doesn't configure the Port80Handler.FQDN field.

Change-Id: Iff56e6127a46c306ca97738d91b217bcab32a582
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-05 13:01:30 -07:00
Denton Gentry
c6ac82e3a6 hostinfo,distro: Identify Western Digital MyCloud devices.
root@WDMyCloud HD_a2 # ./tailscale debug hostinfo
{
  "IPNVersion": "1.25.0-dev20220605-t7fea52e02",
  "OS": "linux",
  "OSVersion": "5.22.113",
  "Desktop": false,
  "DeviceModel": "WD My Cloud Gen2: Marvell Armada 375",
  "Hostname": "WDMyCloud",
  "GoArch": "arm"
}

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-05 08:22:42 -07:00
Denton Gentry
0687195bee logpolicy: put Synology logs buffer in /tmp
Ongoing log writing keeps the spinning disks from hibernating.
Fixes https://github.com/tailscale/tailscale/issues/3551

Tested on DSM6 and DSM7.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-04 22:06:14 -07:00
Brad Fitzpatrick
fbc079d82d ipn/ipnlocal: prevent attempting to run SSH on Synology for now
On DSM7 as a non-root user it'll run into problems.

And we haven't tested on DSM6, even though it might work, but I doubt
it.

Updates #3802
Updates tailscale/corp#5468

Change-Id: I75729042e4788f03f9eb82057482a44b319f04f3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-03 13:50:33 -07:00
Brad Fitzpatrick
2bac8b6013 Revert "cmd/tailscale/cli: disallow --ssh on Synology"
This reverts commit 03e3e6abcd
in favor of #4785.

Change-Id: Ied65914106917c4cb8d15d6ad5e093a6299d1d48
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-03 13:49:42 -07:00
Maisem Ali
03e3e6abcd cmd/tailscale/cli: disallow --ssh on Synology
Updates tailscale/corp#5468
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-04 01:39:33 +05:00
Brad Fitzpatrick
a9b4bf1535 ipn/ipnserver, cmd/tailscaled: fix peerapi on Windows
We weren't wiring up netstack.Impl to the LocalBackend in some cases
on Windows. This fixes Windows 7 when run as a service.

Updates #4750 (fixes after pull in to corp repo)

Change-Id: I9ce51b797710f2bedfa90545776b7628c7528e99
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-03 12:24:47 -07:00
Jordan Whited
43f9c25fd2 cmd/tailscale: surface authentication errors in status.Health (#4748)
Fixes #3713

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-06-03 10:52:07 -07:00
Charlotte Brandhorst-Satzkorn
c980bf01be words: The lists continue to drag-on. (#4780)
For Zaku and Pretzel.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-06-03 10:55:17 -04:00
Mihai Parparita
a9f32656f5 control/controlhttp: allow client and server to communicate over WebSockets
We can't do Noise-over-HTTP in Wasm/JS (because we don't have bidirectional
communication), but we should be able to do it over WebSockets. Reuses
derp WebSocket support that allows us to turn a WebSocket connection
into a net.Conn.

Updates #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-02 21:20:54 -07:00
Maisem Ali
80157f3f37 net/dns/resolver: add support for <IPv4>.via-<site-id>
Currently we only support "via-<site-id>.<IPv4>", however that does not
work with Google Chrome which parses `http://via-1.10.0.0.1` as a search
string and not as a URL. This commit introduces "<IPv4>.via-<site-id>"
(`http://10.0.0.1.via-1`) which is parsed correctly by Chrome.

Updates #3616

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-02 23:20:37 +05:00
Brad Fitzpatrick
69b535c01f wgengine/netstack: replace a 1500 with a const + doc
Per post-submit code review feedback of 1336fb740b from @maisem.

Change-Id: Ic5c16306cbdee1029518448642304981f77ea1fd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-02 08:24:35 -07:00
Brad Fitzpatrick
e428bba7a3 ssh/tailssh: add metrics
Updates #3802

Change-Id: Ic9a4b8c51cff6dfe148a1c78bc0e5074195b7f80
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-02 08:18:53 -07:00
Maisem Ali
67325d334e cmd/tailscale/cli: add lose-ssh risk
This makes it so that the user is notified that the action
they are about to take may result in them getting disconnected from
the machine. It then waits for 5s for the user to maybe Ctrl+C out of
it.

It also introduces a `--accept-risk=lose-ssh` flag for automation, which
allows the caller to pre-acknowledge the risk.

The two actions that cause this are:
- updating `--ssh` from `true` to `false`
- running `tailscale down`

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-02 13:14:43 +05:00
Brad Fitzpatrick
1336fb740b wgengine/netstack: make netstack MTU be 1280 also
Updates #3878

Change-Id: I1850085b32c8a40d85607b4ad433622c97d96a8d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-01 12:16:41 -07:00
Maisem Ali
81487169f0 build_docker.sh: pin github.com/tailscale/mkctr
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-01 19:55:06 +05:00
Tobias Klauser
3a926348a4 hostinfo: use ByteSliceToString from golang.org/x/sys/unix
Use unix.ByteSliceToString in osVersionFreebsd and osVersionLinux to
convert the Utsname.Release []byte field to string.

Signed-off-by: Tobias Klauser <tklauser@distanz.ch>
2022-06-01 07:43:44 -07:00
Tobias Klauser
2a61261a5a hostinfo: use Uname from golang.org/x/sys/unix in osVersionLinux
As already done in osVersionFreebsd. This will allow to use the Utsname
fields as []byte for easier conversion to string.

Signed-off-by: Tobias Klauser <tklauser@distanz.ch>
2022-06-01 07:43:44 -07:00
Maisem Ali
928530a112 ipn/ipnlocal: shutdown sshServer on tailscale down
Also lazify SSHServer initialization to allow restarting the server on a
subsequent `tailscale up`

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-30 15:01:22 +05:00
Brad Fitzpatrick
4d85cf586b cmd/tailscale, ipn/ipnlocal: add "peerapi" ping type
For debugging when stuff like #4750 isn't working.

RELNOTE=tailscale ping -peerapi

Change-Id: I9c52c90fb046e3ab7d2b121387073319fbf27b99
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-28 13:47:12 -07:00
Maisem Ali
575aacb1e2 ssh/tailssh: terminate sessions on stdout copy failures
Currently, killing a SCP copy with a Ctrl+C leaves the session hanging
even though the stdout copy goroutine fails with an io.EOF. Taking a
step back, when we are unable to send any more data back to the client
we should just terminate the session as the client will stop getting any
response from the server anyways.

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-28 21:30:54 +05:00
Maisem Ali
7cd8c3e839 ssh/tailssh: terminate sessions when tailscaled shutsdown
Ideally we would re-establish these sessions when tailscaled comes back
up, however we do not do that yet so this is better than leaking the
sessions.

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-28 21:30:54 +05:00
Maisem Ali
760740905e ssh/tailssh: only use login with TTY sessions
Otherwise, the shell exits immediately causing applications like mosh
and VSCode to fail.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-28 21:03:40 +05:00
Brad Fitzpatrick
02e580c1d2 logtail: use http.NewRequestWithContext
Saves some allocs. Not hot, but because we can now.

And a const instead of a var.

Change-Id: Ieb2b64534ed38051c36b2c0aa2e82739d9d0e015
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-27 16:33:43 -07:00
Tom
2903d42921 wgengine/router: delete hardcoded link-local address on Windows (#4740)
Fixes #4647

It seems that Windows creates a link-local address for the TUN driver, seemingly
based on the (fixed) adapter GUID. This results in a fixed MAC address, which
for some reason doesn't handle loopback correctly. Given the derived link-local
address is preferred for lookups (thanks LLMNR), traffic which addresses the
current node by hostname uses this broken address and never works.

To address this, we remove the broken link-local address from the wintun adapter.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-27 14:42:55 -07:00
Aaron Klotz
b005b79236 net/dns, paths, util/winutil: change net/dns/windowsManager NRPT management to support more than 50 domains.
AFAICT this isn't documented on MSDN, but based on the issue referenced below,
NRPT rules are not working when a rule specifies > 50 domains.

This patch modifies our NRPT rule generator to split the list of domains
into chunks as necessary, and write a separate rule for each chunk.

For compatibility reasons, we continue to use the hard-coded rule ID, but
as additional rules are required, we generate new GUIDs. Those GUIDs are
stored under the Tailscale registry path so that we know which rules are ours.

I made some changes to winutils to add additional helper functions in support
of both the code and its test: I added additional registry accessors, and also
moved some token accessors from paths to util/winutil.

Fixes https://github.com/tailscale/coral/issues/63

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-05-27 14:56:09 -06:00
Joel Cressy
c16271fb46 ipn/localapi: implement LoginInteractive via localapi
Updates: #4738

Signed-off-by: Joel Cressy <joel@jtcressy.net>
2022-05-27 06:34:27 -07:00
Denton Gentry
0f95eaa8bb scripts/installer: fix elementaryOS
c2b907c965 moved UBUNTU_VERSION out
of the ubuntu case and into linuxmint, but linuxmint wasn't the
only Ubuntu-based system which needed it. Restore UBUNTU_VERSION
handling in the ubuntu case.

Break elementaryOS out into its own handling so we can get the
version number handling correct for keyring support.
Tested on an elementaryOS 6.1 VM.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-05-27 06:21:15 -07:00
James Tucker
3f686688a6 cmd/tailscaled: fix state path for windows svc
Signed-off-by: James Tucker <james@tailscale.com>
2022-05-26 17:59:50 -07:00
Aaron Klotz
c163b2a3f1 util/winutil, util/winutil/vss: remove winrestore and vss as they are unnecessary.
I wrote this code way back at the beginning of my tenure at Tailscale when we
had concerns about needing to restore deleted machine keys from backups.

We never ended up using this functionality, and the code is now getting in the
way, so we might as well remove it.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-05-26 12:13:36 -06:00
Tom
fc5839864b wgengine/netstack: handle multiple magicDNS queries per UDP socket (#4708)
Fixes: #4686

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-20 13:30:11 -07:00
Brad Fitzpatrick
c81be7b899 control/controlclient: avoid Noise protocol for js/wasm for now
Updates #3157

Change-Id: I04accc09783a68257d28cadde5818bf0724a8013
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-20 12:09:09 -07:00
Denton Gentry
36af49ae7f install.sh: add RHEL9.
Fixes https://github.com/tailscale/tailscale/issues/4718

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-05-20 09:49:39 -07:00
Mihai Parparita
bded712e58 .github/workflows: fix duplicated build target in Wasm action
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-20 09:31:14 -07:00
Mihai Parparita
7cfc6130e5 .github/workflows: add cross-builder for Wasm
For now just checks that we can build cmd/tailscale/cli, will be
broadened once we can actually build more things.

Updates #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-19 20:57:31 -07:00
Mihai Parparita
eda647cb47 cmd/tailscale/cli: fix ssh CLI command breaking the Wasm build
Adds a stub for syscall.Exec when GOOS=js. We also had a separate branch
for Windows, might as well use the same mechanism there too.

For #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-19 20:49:34 -07:00
Brad Fitzpatrick
cc91a05686 ipn/ipnserver: fix build on js/wasm
Broken by 3dedcd1640 but we don't have CI coverage yet.

Updates #3157

Change-Id: Ie8e95ebd36264887fdeed16fc9f25a857d48124b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-19 15:01:27 -07:00
Mihai Parparita
3222bce02d logtail: add instance metadata to the entry logtail
Allows instances that are running with the same machine ID (due to
cloning) to be distinguished.

Also adds sequence numbers to detect duplicates.

For tailscale/corp#5244

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-18 13:57:14 -07:00
Tom
acfe5bd33b net/dns{., resolver}: time out DNS queries after 10 seconds (#4690)
Fixes https://github.com/tailscale/corp/issues/5198

The upstream forwarder will block indefinitely on `udpconn.ReadFrom` if no
reply is recieved, due to the lack of deadline on the connection object.

There still isn't a deadline on the connection object, but the automatic closing
of the context on deadline expiry will close the connection via `closeOnCtxDone`,
unblocking the read and resulting in a normal teardown.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-18 10:40:04 -07:00
Tom
ec4c49a338 types/key: make NodePublic implement Shardable (#4698)
Needed for an experiment in Control.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-17 10:37:25 -07:00
Denton Gentry
53f6c3f9f2 api.md: document preauthorized and tags fields
Updates https://github.com/tailscale/tailscale/issues/2120
Updates https://github.com/tailscale/tailscale/issues/4571
Updates https://github.com/tailscale/tailscale/issues/1369

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-05-17 09:25:13 -07:00
Brad Fitzpatrick
48d43134d7 cmd/tsshd: delete, leaving only forwarding docs
Updates #3802

Change-Id: I89d4d3d68d64af9bc7288a149b4b34f61884f5f4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-16 11:52:44 -07:00
Denton Gentry
afb3f62b01 scripts/installer.sh: add Xen Enterprise
Tested on a VM running Xen Enterprise 8.2.1.
https://xcp-ng.org/
Fixes https://github.com/tailscale/tailscale/issues/4655

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-05-16 08:37:39 -07:00
Brad Fitzpatrick
da601c23e1 ipn/ipnlocal: add missing place where we set the SSH atomic
This fixes the "tailscale up --authkey=... --ssh" path (or any "up"
path that used Start instead of EditPrefs) which wasn't setting the
bit.

Updates #3802

Change-Id: Ifca532ec58296fedcedb5582312dfee884367ed7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-14 12:47:28 -07:00
David Anderson
e1c1d47991 tsweb: memoize the string forms of HTTP response codes.
Saves 1-2 allocs per HTTP request after warmup, thanks to
avoiding strconv and fmt.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-05-13 14:25:17 -07:00
Tom
9343967317 wgengine/filter: preallocate some hot slices in MatchesFromFilterRules (#4672)
Profiling identified this as a fairly hot path for growing a slice.

Given this is only used in control & when a new packet filter is received, this shouldnt be hot in the client.
2022-05-13 13:56:53 -07:00
David Anderson
c48513b2be tsweb: support recording unabridged HTTP status codes as well.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-05-13 13:50:23 -07:00
Mihai Parparita
561f7be434 wgengine/magicsock: remove unused metric
We don't increment the metricRecvData anywhere, just the per-protocol
ones.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-13 11:15:56 -07:00
Mihai Parparita
dd5548771e util/clientmetric: add gauge_ name prefix when uploading names
The prefix is a signal to tsweb to treat this as a gauge metric when
generating the Prometheus version.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-12 09:52:56 -07:00
Mihai Parparita
86069874c9 net/tstun, wgengine: use correct type for counter metrics
We were marking them as gauges, but they are only ever incremented,
thus counter is more appropriate.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-12 09:30:50 -07:00
James Tucker
87b44aa311 go.mod: bump golang.org/x/sys for CVE-2022-29526
Signed-off-by: James Tucker <james@tailscale.com>
2022-05-10 17:57:51 -07:00
James Tucker
4bb7440094 cmd/tailscaled: use --statedir as documented
Enables the behavior described in the statepath flag, where if only
statedir is passed, then state is statedir/tailscaled.state.

Signed-off-by: James Tucker <james@tailscale.com>
2022-05-10 15:13:13 -07:00
Maisem Ali
6dae9e47f9 types/views: remove alloc in hot path
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-10 11:20:00 -07:00
Brad Fitzpatrick
4c75605e23 go.toolchain.rev: bump Tailscale Go toolchain
For bb6009ec7c

(The cherry-picked fix for golang/go#51776)

Change-Id: Ib4a67c0f82256c62989fbba2cc9c15b728313ebd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-10 05:31:13 -07:00
Maisem Ali
395cb588b6 types/views: make SliceOf/MapOf panic if they see a pointer
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-09 19:49:31 -07:00
Maisem Ali
d04afc697c cmd/viewer,types/views: add support for views of maps
Updates #4635

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-09 19:49:31 -07:00
Maisem Ali
5cd56fe8d5 ssh/tailssh: exec into login when launching a shell
This has the added benefit of displaying the MOTD and reducing our
dependency on the DBus interface.

Fixes #4627
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-09 19:17:52 -07:00
Maisem Ali
a253057fc3 ssh/tailssh: refactor incubator flags
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-09 19:17:52 -07:00
Joe Tsai
741ae9956e tstest/integration/vms: use hujson.Standardize instead of hujson.Unmarshal (#4520)
The hujson package transition to just being a pure AST
parser and formatter for HuJSON and not an unmarshaler.

Thus, parse HuJSON as such, convert it to JSON,
and then use the standard JSON unmarshaler.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-05-06 14:16:10 -07:00
Maisem Ali
9f3ad40707 tailcfg: use cmd/viewer instead of cmd/cloner.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-06 10:58:10 -07:00
Maisem Ali
fd99c54e10 tailcfg,all: change structs to []*dnstype.Resolver
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-06 10:58:10 -07:00
Maisem Ali
679415f3a8 tailcfg: move views into tailcfg_view.go
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-06 10:58:10 -07:00
Maisem Ali
c4e9739251 cmd/viewer: add codegen tool for Views
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-06 10:58:10 -07:00
Maisem Ali
e409e59a54 cmd/cloner,util/codegen: refactor cloner internals to allow reuse
Also run go generate again for Copyright updates.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-06 10:58:10 -07:00
Mihai Parparita
025867fd07 util/clientmetric: switch to TestHooks struct for test-only functions (#4632)
Followup to 7966aed1e0 to pick up
review feedback that was accidentally left out.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-06 10:08:57 -07:00
Mihai Parparita
7966aed1e0 util/clientmetric: add test hooks and ResetLastDelta function
Necessary to force flushing of client metrics more aggressively in
dev/test mode.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-05-06 09:52:48 -07:00
Brad Fitzpatrick
35111061e9 wgengine/netstack, ipn/ipnlocal: serve http://100.100.100.100/
For future stuff.

Change-Id: I64615b8b2ab50b57e4eef1ca66fa72e3458cb4a9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-06 07:51:28 -07:00
Tom
d1d6ab068e net/dns, wgengine: implement DNS over TCP (#4598)
* net/dns, wgengine: implement DNS over TCP

Signed-off-by: Tom DNetto <tom@tailscale.com>

* wgengine/netstack: intercept only relevant port/protocols to quad-100

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-05 16:42:45 -07:00
Brad Fitzpatrick
c4f06ef7be client/tailscale: fix ExpandSNIName on non-default LocalClient
It was using a mix.

Found by @maisem.

Change-Id: Ieb79d78608474ac13c2f44e0f3d8997a5665eb13
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-05 12:22:38 -07:00
Brad Fitzpatrick
46cb9d98a3 api.md: update GET tailnet key detail docs to show preauthorized, tags
Fixes #4571

Change-Id: If81471d0d8cb2f659736991ad8612aed2efc174e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-05 12:09:28 -07:00
Brad Fitzpatrick
c1445155ef ssh/tailssh: handle Control-C during hold-and-delegate prompt
Fixes #4549

Change-Id: Iafc61af5e08cd03564d39cf667e940b2417714cc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-05 11:47:08 -07:00
James Tucker
f9e86e64b7 *: use WireGuard where logged, printed or named
Signed-off-by: James Tucker <james@tailscale.com>
2022-05-04 13:36:05 -07:00
Brad Fitzpatrick
2d1849a7b9 tsweb: remove JSONHandlerFunc
It's unused and we've decided it's not what we want.

Change-Id: I425a0104e8869630b498a0adfd0f455876d6f92b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-04 11:52:07 -07:00
Charlotte Brandhorst-Satzkorn
7ee3068f9d words: after a Series of discussions, Bees should be included (#4606)
There has been a lot of talk about Bees at Tailscale recently, and
naturally, with it being Tailscale, we thought to ourselves:

Do Bees have a tail and/or scales?

Tailscale has a long track record of scientific rigor around the
validity of the inclusions on the tails and scales list, and this time
will be no exception.

Our research has found that Bees, in particular the Honey Bee, produces
wax scales on their abdomens and thus should be included. As for tails;

'Stabby-tails' - Tailscale Employee, 2022

No further justification needed, it will be included.

This change includes Bee in both tails.txt and scales.txt.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-05-04 09:20:23 -04:00
Brad Fitzpatrick
3e1f2d01f7 ipn/ipnlocal: move Ping method from IPN bus to LocalBackend (HTTP)
Change-Id: I61759f1dae8d9d446353db54c8b1e13bfffb3287
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-03 15:59:19 -07:00
Maisem Ali
c60cbca371 control/controlclient: store netinfo and hostinfo separately
Currently, when SetNetInfo is called it sets the value on
hostinfo.NetInfo. However, when SetHostInfo is called it overwrites the
hostinfo field which may mean it also clears out the NetInfo it had just
received.
This commit stores NetInfo separately and combines it into Hostinfo as
needed so that control is always notified of the latest values.

Also, remove unused copies of Hostinfo from ipn.Status and
controlclient.Auto.

Updates #tailscale/corp#4824 (maybe fixes)

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-05-03 15:33:01 -07:00
James Tucker
ae483d3446 wgengine, net/packet, cmd/tailscale: add ICMP echo
Updates tailscale/corp#754

Signed-off-by: James Tucker <james@tailscale.com>
2022-05-03 13:03:45 -07:00
Brad Fitzpatrick
66f9292835 client/tailscale: update Client API a bit
Change-Id: I81aa29a8b042a247eac1941038f5d90259569941
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-03 11:30:57 -07:00
Brad Fitzpatrick
512573598a tailcfg: remove some documented DebugFlags that no longer exist
Update tailscale/corp#5007

Change-Id: I3ce5b1c4cd367bae769a5f5a301925a2dac1b3a6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-05-03 08:28:41 -07:00
Tom DNetto
2a0b5c21d2 net/dns/{., resolver}, wgengine: fix goroutine leak on shutdown
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-05-02 10:42:06 -07:00
Tom DNetto
7f45734663 assorted: documentation and readability fixes
This were intended to be pushed to #4408, but in my excitement I
forgot to git push :/ better late than never.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-30 18:42:19 -07:00
Tom DNetto
9e77660931 net/tstun,wgengine/{.,netstack}: handle UDP magicDNS traffic in netstack
This change wires netstack with a hook for traffic coming from the host
into the tun, allowing interception and handling of traffic to quad-100.

With this hook wired, magicDNS queries over UDP are now handled within
netstack. The existing logic in wgengine to handle magicDNS remains for now,
but its hook operates after the netstack hook so the netstack implementation
takes precedence. This is done in case we need to support platforms with
netstack longer than expected.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-30 10:18:59 -07:00
Tom DNetto
dc71d3559f net/tstun,wgengine: split PreFilterOut into multiple hooks
A subsequent commit implements handling of magicDNS traffic via netstack.
Implementing this requires a hook for traffic originating from the host and
hitting the tun, so we make another hook to support this.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-30 10:18:59 -07:00
Tom DNetto
9dee6adfab cmd/tailscaled,ipn/ipnlocal,wgengine/...: pass dns.Manager into netstack
Needed for a following commit which moves magicDNS handling into
netstack.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-30 10:18:59 -07:00
Tom DNetto
5b85f848dd net/dns,net/dns/resolver: refactor channels/magicDNS out of Resolver
Moves magicDNS-specific handling out of Resolver & into dns.Manager. This
greatly simplifies the Resolver to solely issuing queries and returning
responses, without channels.

Enforcement of max number of in-flight magicDNS queries, assembly of
synthetic UDP datagrams, and integration with wgengine for
recieving/responding to magicDNS traffic is now entirely in Manager.
This path is being kept around, but ultimately aims to be deleted and
replaced with a netstack-based path.

This commit is part of a series to implement magicDNS using netstack.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-30 10:18:59 -07:00
Brad Fitzpatrick
a54671529b client/tailscale: move API client for the control admin API
This was work done Nov-Dec 2020 by @c22wen and @chungdaniel.

This is just moving it to another repo.

Co-Authored-By: Christina Wen <37028905+c22wen@users.noreply.github.com>
Co-Authored-By: Christina Wen <christina@tailscale.com>
Co-Authored-By: Daniel Chung <chungdaniel@users.noreply.github.com>
Co-Authored-By: Daniel Chung <daniel@tailscale.com>

Change-Id: I6da3b05b972b54771f796b5be82de5aa463635ca
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-30 09:05:26 -07:00
Brad Fitzpatrick
e3619b890c client/tailscale: rename tailscale.go -> localclient.go
In prep for other stuff.

Change-Id: I82c24946d062d668cab48ca6749776b6ae7025ac
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-29 19:50:40 -07:00
Maisem Ali
3012a2e1ca ssh/tailssh,ipn/ipnlocal: terminate any active sessions on up --ssh=false
Currently the ssh session isn't terminated cleanly, instead the packets
are just are no longer routed to the in-proc SSH server. This makes it
so that clients get a disconnection when the `RunSSH` pref changes to
`false`.

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-29 16:08:27 -07:00
Mihai Parparita
2ec371fe8b ipn: remove FakeExpireAfter Backend function
No callers remain (last one was removed with
tailscale/corp@1c095ae08f), and it's
pretty esoteric.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-04-29 15:34:53 -07:00
Aaron Klotz
d915e0054c cmd/tailscaled: change Windows service shutdown and add optional event logging
Once a stop request is received and the service updates its status to `svc.StopPending`,
it should continue running *until the shutdown sequence is complete*, and then
return out of `(*ipnService).Execute`, which automatically sends a `svc.Stopped`
notification to Windows.

To make this happen, I changed the loop so that it runs until `doneCh` is
closed, and then returns. I also removed a spurious `svc.StopPending` notification
that the Windows Service Control Manager might be interpreting as a request for
more time to shut down.

Finally, I added some optional logging that sends a record of service notifications
to the Windows event log, allowing us to more easily correlate with any Service
Control Manager errors that are sent to the same log.

Change-Id: I5b596122e5e89c4c655fe747a612a52cb4e8f1e0
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-04-29 15:13:11 -07:00
Mihai Parparita
316523cc1e ipn: remove enforceDefaults option from PrefsFromBytes
The Mac client was using it, but it had the effect of the `RouteAll`
("Use Tailscale subnets") pref always being enabled at startup,
regardless of the persisted value.

enforceDefaults was added to handle cases from ~2 years ago where
we ended up with persisted `"RouteAll": false` values in the keychain,
but that should no longer be a concern. New users will get the default
of it being enabled via `NewPrefs`.

There will be a corresponding Mac client change to stop passing in
enforceDefaults.

For #3962

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-04-29 14:08:26 -07:00
Brad Fitzpatrick
87ba528ae0 client/tailscale: move/copy all package funcs to new LocalClient type
Remove all global variables, and clean up tsnet and cmd/tailscale's usage.

This is in prep for using this package for the web API too (it has the
best package name).

RELNOTE=tailscale.com/client/tailscale package refactored w/ LocalClient type

Change-Id: Iba9f162fff0c520a09d1d4bd8862f5c5acc9d7cd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-29 13:57:52 -07:00
Brad Fitzpatrick
373176ea54 util/codegen: format generated code with goimports, not gofmt
goimports is a superset of gofmt that also groups imports.
(the goimports tool also adds/removes imports as needed, but that
part is disabled here)

Change-Id: Iacf0408dfd9497f4ed3da4fa50e165359ce38498
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-29 13:35:45 -07:00
Brad Fitzpatrick
6bed781259 all: gofmt all
Well, goimports actually (which adds the normal import grouping order we do)

Change-Id: I0ce1b1c03185f3741aad67c14a7ec91a838de389
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-29 13:06:04 -07:00
Brad Fitzpatrick
deb56f276e Revert "api: document preauthorized auth keys"
This reverts commit dd6472d4e8.

Reason: it appears I was just really really wrong or confused.

We added it to the old internal API used by the website instead,
not to the "v2" API.

Updates #2120
Updates #4571

Change-Id: I744a72b9193aafa7b526fd760add52148a377e83
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-29 08:47:18 -07:00
Mihai Parparita
cfe68d0a86 safesocket: log warning when running sandboxed Mac binary as root
It won't work, provide a clue in the error output.

Fixes #3063

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-04-28 16:22:19 -07:00
Brad Fitzpatrick
6f5b91c94c go.mod: tidy
Change-Id: If3b5fe42e7dd7858dcce02a3a24a5e59736815a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-28 14:32:43 -07:00
Brad Fitzpatrick
2336c73d4d go.mod: tidy
Change-Id: If3b5fe42e7dd7858dcce02a3a24a5e59736815a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-28 14:29:53 -07:00
James Tucker
96fec4b969 net/tshttpproxy: synology: pick proxy by scheme
This updates the fix from #4562 to pick the proxy based on the request
scheme.

Updates #4395, #2605, #4562
Signed-off-by: James Tucker <james@tailscale.com>
2022-04-28 11:56:37 -07:00
Maisem Ali
eff6a404a6 net/tshttpproxy: use http as the scheme for proxies
Currently we try to use `https://` when we see `https_host`, however
that doesn't work and results in errors like `Received error: fetch
control key: Get "https://controlplane.tailscale.com/key?v=32":
proxyconnect tcp: tls: first record does not look like a TLS handshake`

This indiciates that we are trying to do a HTTPS request to a HTTP
server. Googling suggests that the standard is to use `http` regardless
of `https` or `http` proxy

Updates #4395, #2605

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-28 10:45:33 -07:00
Walter Poupore
71d401cc4e api.md: remove descriptions from TOC (#4561) 2022-04-28 09:36:26 -07:00
Brad Fitzpatrick
1237000efe control/controlhttp: don't assume port 80 upgrade response will work
Just because we get an HTTP upgrade response over port 80, don't
assume we'll be able to do bi-di Noise over it. There might be a MITM
corp proxy or anti-virus/firewall interfering. Do a bit more work to
validate the connection before proceeding to give up on the TLS port
443 dial.

Updates #4557 (probably fixes)

Change-Id: I0e1bcc195af21ad3d360ffe79daead730dfd86f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-28 09:14:41 -07:00
Brad Fitzpatrick
488e63979e api.md: document new ACL validate mode
Updates tailscale/corp#4932

Change-Id: Ie176ee79595c3b56d3376f1d81a441f9272e6ed4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-27 15:58:37 -07:00
Maisem Ali
5a1ef1bbb9 net/tsdial: add SystemDial as a wrapper on netns.Dial
The connections returned from SystemDial are automatically closed when
there is a major link change.

Also plumb through the dialer to the noise client so that connections
are auto-reset when moving from cellular to WiFi etc.

Updates #3363

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-27 12:02:36 -07:00
Brad Fitzpatrick
e38d3dfc76 control/controlhttp: start port 443 fallback sooner if 80's stuck
Fixes #4544

Change-Id: I39877e71915ad48c6668351c45cd8e33e2f5dbae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-27 10:34:53 -07:00
Maisem Ali
637cc1b5fc ipn/ipnlocal/peerapi: add endpoint to list local interfaces
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-27 06:44:58 -07:00
James Tucker
1aa75b1c9e wgengine/netstack: always set TCP keepalive
Setting keepalive ensures that idle connections will eventually be
closed. In userspace mode, any application configured TCP keepalive is
effectively swallowed by the host kernel, and is not easy to detect.
Failure to close connections when a peer tailscaled goes offline or
restarts may result in an otherwise indefinite connection for any
protocol endpoint that does not initiate new traffic.

This patch does not take any new opinion on a sensible default for the
keepalive timers, though as noted in the TODO, doing so likely deserves
further consideration.

Update #4522

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-26 19:29:08 -07:00
Brad Fitzpatrick
adcb7e59d2 control/controlclient: fix log print with always-empty key
In debugging #4541, I noticed this log print was always empty.
The value printed was always zero at this point.

Updates #4541

Change-Id: I0eef60c32717c293c1c853879446be65d9b2cef6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 19:27:20 -07:00
Brad Fitzpatrick
c88506caa6 ipn/ipnlocal: add Wake-on-LAN function to peerapi
No CLI support yet. Just the curl'able version if you know the peerapi
port. (like via a TSMP ping)

Updates #306

Change-Id: I0662ba6530f7ab58d0ddb24e3664167fcd1c4bcf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 15:20:59 -07:00
Brad Fitzpatrick
3f7cc3563f ipn: always treat login.tailscale.com as controlplane.tailscale.com
Like 888e50e1, but more aggressive.

Updates #4538 (likely fixes)
Updates #3488

Change-Id: I3924eee9110e47bdba926ce12954253bf2413040
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 15:16:32 -07:00
Brad Fitzpatrick
c6c752cf64 net/tshttpproxy: fix typo
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 08:14:50 -07:00
Brad Fitzpatrick
50eb8c5add cmd/tailscale: mostly fix 'tailscale ssh' on macOS (sandbox)
Still a little wonky, though. See the tcsetattr error and inability to
hit Ctrl-D, for instance:

    bradfitz@laptop ~ % tailscale.app ssh foo@bar
    tcsetattr: Operation not permitted
    # Authentication checked with Tailscale SSH.
    # Time since last authentication: 1h13m22s
    foo@bar:~$ ^D
    ^D
    ^D

Updates #4518
Updates #4529

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 07:40:42 -07:00
Brad Fitzpatrick
48e5f4ff88 cmd/tailscale/cli: add 'debug stat' subcommand
For debugging what's visible inside the macOS sandbox.

But could also be useful for giving users portable commands
during debugging without worrying about which OS they're on.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-26 07:30:08 -07:00
Brad Fitzpatrick
21413392cf safesocket: fix CLI on standalone mac GUI build
Tested three macOS Tailscale daemons:

- App Store (Network Extension)
- Standalone (macsys)
- tailscaled

And two types of local IPC each:

- IPN
- HTTP

And two CLI modes:

- sandboxed (running the GUI binary as the CLI; normal way)
- open source CLI hitting GUI (with #4525)

Bonus: simplifies the code.

Fixes tailscale/corp#4559

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-25 21:47:00 -07:00
Brad Fitzpatrick
3601b43530 ipn: add IPCVersion override func
I've done this a handful of times in the past and again today.
Time to make it a supported thing for the future.

Used while debugging tailscale/corp#4559 (macsys CLI issues)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-25 20:51:05 -07:00
James Tucker
928d1fddd2 cmd/tailscale: s/-authkey/-auth-key/ in help text
Signed-off-by: James Tucker <james@tailscale.com>
2022-04-25 17:30:50 -07:00
Tom DNetto
5fb8e01a8b net/dns/resolver: add metric for number of truncated dns packets
Updates #2067

This should help us determine if more robust control of edns parameters
+ implementing answer truncation is warranted, given its likely complexity.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-25 13:05:28 -07:00
Maisem Ali
80ba161c40 wgengine/monitor: do not ignore changes to pdp_ip*
One current theory (among other things) on battery consumption is that
magicsock is resorting to using the IPv6 over LTE even on WiFi.
One thing that could explain this is that we do not get link change updates
for the LTE modem as we ignore them in this list.
This commit makes us not ignore changes to `pdp_ip` as a test.

Updates #3363

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-25 12:17:00 -07:00
Maisem Ali
1a19aed410 ipn/ipnlocal: do not initialize peer api listeners when shutting down
Updates tailscale/corp#4824

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-25 11:08:03 -07:00
Brad Fitzpatrick
e97209c6bf net/dns: add tailscaled-on-macOS DNS OSConfigurator
This populates DNS suffixes ("ts.net", etc) in /etc/resolver/* files
to point to 100.100.100.100 so MagicDNS works.

It also sets search domains.

Updates #4276

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-23 20:43:41 -07:00
Maisem Ali
bbca2c78cb tsnet: fix mem.Store check for normal nodes
There was a typo in the check it was doing `!ok` instead of `ok`, this
restructures it a bit to read better.

Fixes #4506

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-22 23:52:16 -07:00
Denton Gentry
d819bb3bb0 VERSION.txt: This is 1.25.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-22 13:26:24 -07:00
Maisem Ali
2265587d38 wgengine/{,magicsock}: add metrics for rebinds and restuns
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-22 11:55:46 -07:00
Tom DNetto
78fededaa5 net/dns/resolver: support magic resolution of via-<siteid>.<ip4> domains
Updates #3616

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-22 09:21:35 -07:00
Brad Fitzpatrick
910ae68e0b util/mak: move tailssh's mapSet into a new package for reuse elsewhere
Change-Id: Idfe95db82275fd2be6ca88f245830731a0d5aecf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 21:20:10 -07:00
James Tucker
c2eff20008 ssh/tailssh: avoid user ssh configuration in tests
Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 19:17:34 -07:00
James Tucker
700bd37730 tshttpproxy: support synology proxy configuration
Fixes #4395
Fixes #2605

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 18:39:00 -07:00
Maisem Ali
90b5f6286c cmd/tailscale: use double quotes in the ssh subcommands
Single-quote escaping is insufficient apparently.

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-21 17:43:04 -07:00
Maisem Ali
db70774685 cmd/tailscale/cli: do not use syscall.Exec from macOS sandbox
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-21 17:07:18 -07:00
Tom DNetto
37c94c07cd shell.nix: update go toolchain
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-21 15:47:34 -07:00
David Anderson
a364bf2b62 ssh/tailssh: various typo fixes, clarifications.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-04-21 15:04:13 -07:00
Brad Fitzpatrick
c994eba763 ssh/tailssh: simplify matchRule with Reject rules
Updates #3802

Change-Id: I59fe111eef5ac8abbcbcec922e293712a65a4830
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 15:04:02 -07:00
Maisem Ali
31094d557b ssh/tailssh: chmod the auth socket to be only user accessible
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-21 14:49:22 -07:00
Maisem Ali
337c77964b ssh/tailssh: set groups and gid in the incubated process
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-21 14:48:34 -07:00
Brad Fitzpatrick
8ac4d52b59 ssh/tailssh: filter accepted environment variables
Noted by @danderson

Updates #3802

Change-Id: Iac70717ed57f11726209ac1ea93ddc6696605f94
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 14:44:46 -07:00
Brad Fitzpatrick
89832c1a95 tailcfg: fix typo in SessionDuration field name
Noted by @danderson.

Updates #3802

Change-Id: Ide15f3f28e30f6abb5c94d7dcd218bd9482752a0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 14:19:58 -07:00
Maisem Ali
695f8a1d7e ssh/tailssh: add support for sftp
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-21 10:52:22 -07:00
Brad Fitzpatrick
53588f632d Revert "wgengine/router,util/kmod: load & log xt_mark"
This reverts commit 8d6793fd70.

Reason: breaks Android build (cgo/pthreads addition)

We can try again next cycle.

Change-Id: I5e7e1730a8bf399a8acfce546a6d22e11fb835d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 09:53:23 -07:00
Tom DNetto
df26c63793 net/dns/resolver, net/tsaddr: fix reverse lookups in 4to6 IP range
Fixes #4439

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-21 09:37:21 -07:00
James Tucker
8d6793fd70 wgengine/router,util/kmod: load & log xt_mark
Attempt to load the xt_mark kernel module when it is not present. If the
load fails, log error information.

It may be tempting to promote this failure to an error once it has been
in use for some time, so as to avoid reaching an error with the iptables
invocation, however, there are conditions under which the two stages may
disagree - this change adds more useful breadcrumbs.

Example new output from tailscaled running under my WSL2:

```
router: ensure module xt_mark: "/usr/sbin/modprobe xt_mark" failed: exit status 1; modprobe: FATAL: Module xt_mark not found in directory /lib/modules/5.10.43.3-microsoft-standard-WSL2
```

Background:

There are two places to lookup modules, one is `/proc/modules` "old",
the other is `/sys/module/` "new".

There was query_modules(2) in linux <2.6, alas, it is gone.

In a docker container in the default configuration, you would get
/proc/modules and /sys/module/ both populated. lsmod may work file,
modprobe will fail with EPERM at `finit_module()` for an unpriviliged
container.

In a priviliged container the load may *succeed*, if some conditions are
met. This condition should be avoided, but the code landing in this
change does not attempt to avoid this scenario as it is both difficult
to detect, and has a very uncertain impact.

In an nspawn container `/proc/modules` is populated, but `/sys/module`
does not exist. Modern `lsmod` versions will fail to gather most module
information, without sysfs being populated with module information.

In WSL2 modules are likely missing, as the in-use kernel typically is
not provided by the distribution filesystem, and WSL does not mount in a
module filesystem of its own. Notably the WSL2 kernel supports iptables
marks without listing the xt_mark module in /sys/module, and
/proc/modules is empty.

On a recent kernel, we can ask the capabilities system about SYS_MODULE,
that will help to disambiguate between the non-privileged container case
and just being root. On older kernels these calls may fail.

Update #4329

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-20 22:21:35 -07:00
Brad Fitzpatrick
f7cb6630e7 tailcfg: document SSHPrincipal.PubKeys URL expansions
From f74ee80abe which lacked docs.

Updates #3802

Change-Id: Ia7df05a486ae383cc6d9aca9dfe487b04e243ad5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 20:12:37 -07:00
Brad Fitzpatrick
5b4154342e ssh/tailssh: fix double SSH-2.0- prefix in greeting banner
gliderlabs/ssh was already adding the "SSH-2.0-" prefix.

Updates #3802

Change-Id: I19a1cd9308371a2898e7883cf26e94c9b54bab29
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 20:08:39 -07:00
Brad Fitzpatrick
7a097ccc83 ipn/ipnlocal: close peerapi listeners on LocalBackend.Shutdown
For tests.

Now that we can always listen (whereas we used to fail prior to
a2c330c496), some goroutine leak
checks were failing in tests in another repo after that change.

Change-Id: Id95a4b71167eca61962a48616d79741b9991e0bc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 19:25:42 -07:00
Maisem Ali
2b8b887d55 ssh/tailssh: send banner messages during auth, move more to conn
(VSCode Live Share between Brad & Maisem!)

Updates #3802

Change-Id: Id8edca4481b0811debfdf56d4ccb1a46f71dd6d3
Co-Authored-By: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 18:34:11 -07:00
Denton Gentry
13f75b9667 scripts/install: add Alma Linux.
Tested using an Alma Linux 8.5 VM.
Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
c2b907c965 scripts/installer: support LinuxMint Debian.
The primary distribution for LinuxMint is based on Ubuntu,
but there is an alternate Debian-based distribution called
LMDE. Both variations identify themselves as "linuxmint"

We added UBUNTU_VERSION to the Ubuntu handling for linuxmint,
the only distribution so far found to do this. Instead, split
linuxmint out into its own case and use either UBUNTU_VERSION
or DEBIAN_VERSION, whichever is present.

Tested on an LMDE 5 (elsie) VM.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
61868f281e scripts/installer: call emerge with --ask=n
Fixes https://github.com/tailscale/tailscale/issues/4354

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
db7da6622a scripts/installer: add ParrotOS support
Support ParrotSec https://parrotsec.org/
Tested using a Parrot 5.0 VM.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Brad Fitzpatrick
d413850bd7 cmd/tailscale: add "debug via" subcommand to do CIDR math for via ranges
$ tailscale debug via 0xb 10.2.0.0/16
fd7a:115c:a1e0:b1a:0:b:a02:0/112
$ tailscale debug via fd7a:115c:a1e0:b1a:0:b:a02:0/112
site 11 (0xb), 10.2.0.0/16

Previously: 3ae701f0eb

This adds a little debug tool to do CIDR math to make converting between
those ranges easier for now.

Updates #3616

Change-Id: I98302e95d17765bfaced3ecbb71cbd43e84bff46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 14:47:48 -07:00
Brad Fitzpatrick
f74ee80abe ssh/tailssh: support expansions in public key fetch URL too
Updates #3802

Change-Id: I5aa98bdab14fd1c1c00ba63b93f8d7e670f72437
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 14:04:50 -07:00
Maisem Ali
14d077fc3a ssh/tailssh: terminate ssh auth early if no policy can match
Also bump github.com/tailscale/golang-x-crypto/ssh

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 13:44:04 -07:00
Brad Fitzpatrick
a2c330c496 ipn/ipnlocal: use the fake peerapi listener as fallback if netstack available
The previous commit (1b89662eff) this for Android, but we can also use
this on any platform if we we would otherwise fail.

Change-Id: I4cd78b40e9e77fca5cc8e717dd48ac173101bed4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 13:41:44 -07:00
Maisem Ali
136f30fc92 wgengine/monitor: split the unexpected stringification log line
It unfortuantely gets truncated because it's too long, split it into 3
different log lines to circumvent truncation.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 12:32:15 -07:00
Maisem Ali
8e40bfc6ea wgengine/monitor: ignore OS-specific uninteresting interfaces
Currently we ignore these interfaces in the darwin osMon but then would consider it
interesting when checking if anything had changed.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 12:32:15 -07:00
Brad Fitzpatrick
1b89662eff ipn/ipnlocal: make peerapi listener on Android avoid the kernel
We intercept the peerapi port in netstack anyway, so there's no reason
the linux kernel on Android needs to know about it. It's only getting
in the way and causing problems for reasons we don't fully understand.
But we don't even need to understand it because it's not relevant
anymore.

Instead, provide a dummy net.Listener that just sits and blocks to
pacify the rest of the code that assumes it can be stuck in a
Listener.Accept call and call Listener.Close and Listener.Addr.

We'll likely do this for all platforms in the future, if/when we also
link in netstack on iOS.

Updates #4449
Updates #4293
Updates #3986

Change-Id: Ic2d3fe2f3cee60fc527356a3368830f17aeb75ae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 12:31:12 -07:00
Brad Fitzpatrick
cf9b9a7fec tstest/iosdeps: add test for forbidden iOS dependencies
Fixes #4463

Change-Id: I8305710e8a075263ae9a88a29624b19032d5beeb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 12:30:23 -07:00
Brad Fitzpatrick
8b81254992 ipn/ipnlocal: reject tailscale up --ssh if disabled on tailnet
Updates #3802

Change-Id: I3f1e839391fe9b28270f506f4bb8d8e3d36716f5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 11:38:27 -07:00
Brad Fitzpatrick
0ce67ccda6 wgengine/router: make supportsV6NAT check catch more cases
Updates #4459

Change-Id: Ic27621569d2739298e652769d10e38608c6012be
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 10:28:33 -07:00
Xe Iaso
fc2f628d4c cmd/nginx-auth: maintainer scripts and tailnet checking (#4460)
* cmd/nginx-auth: add maintainer scripts

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

* cmd/nginx-auth: add Expected-Tailnet header and documentation

Signed-off-by: Xe <xe@tailscale.com>
2022-04-20 13:06:05 -04:00
Blake Mizerany
33fa43252e cmd/proxy-to-grafana: prevent premature termination
This commit changes proxy-to-grafana to report errors while polling for
tailscaled status instead of terminating at the first sign of an error.
This allows tailscale some time to come up before the proxy decides to
give up.

Signed-off-by: Blake Mizerany <blake.mizerany@gmail.com>
2022-04-19 13:31:36 -07:00
Tom DNetto
c8f4dfc8c0 derp/derphttp,net/netcheck: improve netcheck behavior under MITM proxies
In cases where tailscale is operating behind a MITM proxy, we need to consider
that a lot more of the internals of our HTTP requests are visible and may be
used as part of authorization checks. As such, we need to 'behave' as closely
as possible to ideal.

 - Some proxies do authorization or consistency checks based the on Host header
   or HTTP URI, instead of just the IP/hostname/SNI. As such, we need to
   construct a `*http.Request` with a valid URI everytime HTTP is going to be
   used on the wire, even if its over TLS.
   Aside from the singular instance in net/netcheck, I couldn't find anywhere
   else a http.Request was constructed incorrectly.

 - Some proxies may deny requests, typically by returning a 403 status code. We
   should not consider these requests as a valid latency check, so netcheck
   semantics have been updated to consider >299 status codes as a failed probe.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-19 12:47:57 -07:00
Brad Fitzpatrick
cc575fe4d6 net/dns: schedule DoH upgrade explicitly, fix Resolver.Addr confusion
Two changes in one:

* make DoH upgrades an explicitly scheduled send earlier, when we come
  up with the resolvers-and-delay send plan. Previously we were
  getting e.g.  four Google DNS IPs and then spreading them out in
  time (for back when we only did UDP) but then later we added DoH
  upgrading at the UDP packet layer, which resulted in sometimes
  multiple DoH queries to the same provider running (each doing happy
  eyeballs dialing to 4x IPs themselves) for each of the 4 source IPs.
  Instead, take those 4 Google/Cloudflare IPs and schedule 5 things:
  first the DoH query (which can use all 4 IPs), and then each of the
  4 IPs as UDP later.

* clean up the dnstype.Resolver.Addr confusion; half the code was
  using it as an IP string (as documented) as half was using it as
  an IP:port (from some prior type we used), primarily for tests.
  Instead, document it was being primarily an IP string but also
  accepting an IP:port for tests, then add an accessor method on it
  to get the IPPort and use that consistently everywhere.

Change-Id: Ifdd72b9e45433a5b9c029194d50db2b9f9217b53
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 12:00:22 -07:00
Brad Fitzpatrick
e3a4952527 net/dns/resolver: count errors when racing DNS queries, fail earlier
If all N queries failed, we waited until context timeout (in 5
seconds) to return.

This makes (*forwarder).forward fail fast when the network's
unavailable.

Change-Id: Ibbb3efea7ed34acd3f3b29b5fee00ba8c7492569
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 11:07:31 -07:00
Brad Fitzpatrick
d9efbd97cb net/dns: remove an unused function
Change-Id: I7c920c76223ffac37954ef2a18754afc52177598
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 10:53:54 -07:00
Brad Fitzpatrick
c13be0c509 tailcfg: clarify how SSHPolicy.Rules are evaluated between auth phases
Updates #3802

Change-Id: I321183a8a2b065a40dca8dd95ca90cd822a17ff8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 22:03:52 -07:00
Maisem Ali
91a187bf87 ssh/tailssh: make checkStillValid also consider username changes
Currently if the policy changes and the session is logged in with local
user "u1" and the new policy says they can only login with "u2" now, the
user doesn't get kicked out because they had requested
`rando@<ssh-host>` and the defaulting had made that go to `u1`.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-18 16:34:06 -07:00
Maisem Ali
a04eebf59f ipn/ipnlocal: also use SSHPolicies when updating filterHash
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-18 16:34:06 -07:00
Joe Tsai
d201d217df go.toolchain.rev: update to go1.18.1 (#4438)
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-04-18 14:42:58 -07:00
Tom DNetto
24cd26534f hostinfo, tailcfg: add desktop detection on Linux to hostinfo
From the machines tab its hard to differenciate desktop Linux installs from
server Linux installs. Transmitting this information should make this
determination a lot easier.

Due to the reality that tailscaled is likely a system process, the standard
checks based on XDG_SESSION_TYPE or DISPLAY environment variables are not
possible (those variables won't be set). Instead, we look for listening
unix sockets that are typical of desktop installs.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-18 14:22:15 -07:00
Brad Fitzpatrick
9f1dd716e8 tailcfg, logtail: provide Debug bit to disable logtail
For people running self-hosted control planes who want a global
opt-out knob instead of running their own logcatcher.

Change-Id: I7f996c09f45850ff77b58bfd5a535e197971725a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 13:53:13 -07:00
Brad Fitzpatrick
ecea6cb994 net/dns/resolver: make DoH dialer use existing dnscache happy eyeball dialer
Simplify the ability to reason about the DoH dialing code by reusing the
dnscache's dialer we already have.

Also, reduce the scope of the "ip" variable we don't want to close over.

This necessarily adds a new field to dnscache.Resolver:
SingleHostStaticResult, for when the caller already knows the IPs to be
returned.

Change-Id: I9f2aef7926f649137a5a3e63eebad6a3fffa48c0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 13:18:39 -07:00
Brad Fitzpatrick
e96dd00652 ipn/ipnlocal: add capability for debugging peers over peerapi
The default is still users can debug their own nodes. But like
cd916b728b did, this adds support for admins to grant additional
capabilities with the new tailcfg.CapabilityDebugPeer cap.

Updates #4217

Change-Id: Ifce3d9a1f8e8845797970a4f97b393194663d35f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 10:21:42 -07:00
Maisem Ali
945879fa38 cmd/tailscale: [ssh] enable StrictHostKeyChecking mode
Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-18 10:11:36 -07:00
Brad Fitzpatrick
8f5e5bff1e cmd/tailscale, etc: make "tailscale up --ssh" fail fast when unavailable
Fail on unsupported platforms (must be Linux or macOS tailscaled with
WIP env) or when disabled by admin (with TS_DISABLE_SSH_SERVER=1)

Updates #3802

Change-Id: I5ba191ed0d8ba4ddabe9b8fc1c6a0ead8754b286
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 09:45:46 -07:00
Brad Fitzpatrick
f0e2272e04 cmd/tailscale: unhide 'up --ssh' behind WIP env var
Updates #3802

Change-Id: I99c550c2e4450640b0ee6ab060f178dde1360553
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 07:55:16 -07:00
Brad Fitzpatrick
93221b4535 ssh/tailssh: cache public keys fetched from URLs
Updates #3802

Change-Id: I96715bae02bce6ea19f16b1736d1bbcd7bcf3534
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 07:46:38 -07:00
Maisem Ali
3ffd88a84a wgengine/monitor: do not set timeJumped on iOS/Android
In `(*Mon).Start` we don't run a timer to update `(*Mon).lastWall` on iOS and
Android as their sleep patterns are bespoke. However, in the debounce
goroutine we would notice that the the wall clock hadn't been updated
since the last event would assume that a time jump had occurred. This would
result in non-events being considered as major-change events.

This commit makes it so that `(*Mon).timeJumped` is never set to `true`
on iOS and Android.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-17 23:46:17 -07:00
Brad Fitzpatrick
ade7bd8745 ssh/tailssh: close sessions on policy change if no longer allowed
Updates #3802

Change-Id: I98503c2505b77ac9d0cc792614fcdb691761a70c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-17 15:29:44 -07:00
Brad Fitzpatrick
4ec83fbad6 ipn/ipnlocal: only call updateFilter with mutex held
And rename to updateFilterLocked to prevent future mistakes.

Fixes #4427

Change-Id: I4d37b90027d5ff872a339ce8180f5723704848dc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-17 15:29:24 -07:00
Brad Fitzpatrick
cd916b728b ipn/ipnlocal: add start of inter-user Taildrop
Controlled by server-sent capability policy.

To be initially used for SSH servers to record sessions to other
nodes. Not yet productized into something user-accessible. (Notably,
the list of Taildrop targets from the sender side isn't augmented
yet.) This purely permits expanding the set of expands a node will
accept a drop from.

Updates #3802
Updates #4217

Change-Id: Id7a5bccd686490f8ef2cdc7dae7c07c440dc0085
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-17 10:07:07 -07:00
Brad Fitzpatrick
f4f76eb275 net/dnsfallback: update from 'go generate'
Change-Id: I93e0e6d9a4a471953c1ffef07f32605c5724aed8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-17 10:06:09 -07:00
Brad Fitzpatrick
16f3520089 all: add arbitrary capability support
Updates #4217

RELNOTE=start of WhoIsResponse capability support

Change-Id: I6522998a911fe49e2f003077dad6164c017eed9b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-17 09:01:53 -07:00
James Tucker
c591c91653 tailcfg, control/controlclient: TSMP & disco pings
tailcfg.PingResponse formalizes the TSMP & disco response message, and
controlclient is wired to send POST responses containing
tailcfg.PingResponse for TSMP and disco PingRequests.

Updates tailscale/corp#754

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-15 22:36:51 -07:00
James Tucker
67192a2323 go.mod: bump u-root
We only use the termios subpackage, so we're unaffected by
CVE-2020-7665, but the bump will let dependabot return to slumber.

Fixes https://github.com/tailscale/tailscale/security/dependabot/2

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-15 14:13:22 -07:00
Brad Fitzpatrick
8ee044ea4a ssh/tailssh: make the SSH server a singleton, register with LocalBackend
Remove the weird netstack -> tailssh dependency and instead have tailssh
register itself with ipnlocal when linked.

This makes tailssh.server a singleton, so we can have a global map of
all sessions.

Updates #3802

Change-Id: Iad5caec3a26a33011796878ab66b8e7b49339f29
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-15 13:45:39 -07:00
Brad Fitzpatrick
da14e024a8 tailcfg, ssh/tailssh: optionally support SSH public keys in wire policy
And clean up logging.

Updates #3802

Change-Id: I756dc2d579a16757537142283d791f1d0319f4f0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-15 13:36:57 -07:00
Brad Fitzpatrick
df9ce972c7 tailcfg, ipn/ipnlocal: add debug flag to enable one-big-CGNAT/10 route
To experiment with avoiding Chrome ERR_NETWORK_CHANGED errors on route
changes.

Updates #3102

Change-Id: I339da14c684fdac45ac261566aa21bf2198672ff
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-14 14:47:42 -07:00
phirework
52d32c94d8 net/dns/publicdns: add missing call to sync.Once.Do (#4410)
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-04-14 17:45:58 -04:00
phirework
83c734a6e0 net/dns, util/publicdns: extract public DNS mapping into own package (#4405)
This extracts DOH mapping of known public DNS providers in
forwarder.go into its own package, to be consumed by other repos

Signed-off-by: Jenny Zhang <jz@tailscale.com>
2022-04-14 17:15:54 -04:00
James Tucker
8de7f9bff7 tailscaled: no longer tune gcpercent
Usage of userspace-networking is increasing, and the aggressive GC
tuning causes a significant reduction in performance in that mode.

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-14 11:41:45 -07:00
Xe Iaso
4f1d6c53cb cmd/nginx-auth: create new Tailscale NGINX auth service (#4400)
This conforms to the NGINX subrequest result authentication protocol[1]
using the NGINX module `ngx_http_auth_request_module`. This is based on
the example that @peterkeen provided on Twitter[2], but with several
changes to make things more tightly locked down:

* This listens over a UNIX socket instead of a TCP socket to prevent
  leakage to the network
* This uses systemd socket activation so that systemd owns the socket
  and can then lock down the service to the bare minimum required to do
  its job without having to worry about dropping permissions
* This provides additional information in HTTP response headers that can
  be useful for integrating with various services
* This has a script to automagically create debian and redhat packages
  for easier distribution

This will be written about on the Tailscale blog. There is more
information in README.md.

[1]: https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/
[2]: https://github.com/peterkeen/tailscale/blob/main/cmd/nginx-auth-proxy/nginx-auth-proxy.go

Signed-off-by: Xe Iaso <xe@tailscale.com>
2022-04-14 11:55:35 -04:00
Maisem Ali
50b4b8b2c6 ipn/ipnlocal: make peerIPs return a sorted slice
Currently peerIPs doesn't do any sorting of the routes it returns. This
is typically fine, however imagine the case of an HA subnet router
failover. When a route R moves from peer A to peer B, the output of
peerIPs changes. This in turn causes all the deephash check inside
wgengine to fail as the hashed value of [R1, R2] is different than
the hashed value of [R2, R1]. When the hash check failes, it causes
wgengine to reconfigure all routes in the OS. This is especially
problematic for macOS and iOS where we use the NetworkExtension.

This commit makes it that the peerIPs are always sorted when returned,
thus making the hash be consistent as long as the list of routes remains
static.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-13 16:30:17 -07:00
Brad Fitzpatrick
a49d8d5200 Revert ".github/workflows: work around golang/go#51629"
This reverts commit 2a412ac9ee.

Updates #4194

Change-Id: I0098b66b71d20bea301ca79058c1cdd201237dd0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-13 14:27:01 -07:00
Brad Fitzpatrick
09c5c9eb83 go.mod: bump x/tools for go/packages generics fix
Updates #4194

Change-Id: Ia992ffb14210d5ad53f8f98d12b80d64080998e6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-13 14:27:01 -07:00
Tom DNetto
dec68166e4 tstest/integration/vms: smoke test derphttp through mitm proxies
Updates #4377

Very smoky/high-level test to ensure that derphttp internals play well
with an agressive (stare + bump) meddler-in-the-middle proxy.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-12 13:24:29 -07:00
Ilya Mateyko
2748750aa2 ipn/ipnstate: make status page more mobile-friendly
Signed-off-by: Ilya Mateyko <me@astrophena.name>
2022-04-12 12:33:31 -07:00
Maisem Ali
c87ed52ad4 cmd/tailscale: add id-token subcommand
RELNOTE=Initial support for getting OIDC ID Tokens

Updates tailscale/corp#4347

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-12 12:04:04 -07:00
Brad Fitzpatrick
3ae701f0eb net/tsaddr, wgengine/netstack: add IPv6 range that forwards to site-relative IPv4
This defines a new magic IPv6 prefix, fd7a:115c:a1e0:b1a::/64, a
subset of our existing /48, where the final 32 bits are an IPv4
address, and the middle 32 bits are a user-chosen "site ID". (which
must currently be 0000:00xx; the top 3 bytes must be zero for now)

e.g., I can say my home LAN's "site ID" is "0000:00bb" and then
advertise its 10.2.0.0/16 IPv4 range via IPv6, like:

    tailscale up --advertise-routes=fd7a:115c:a1e0:b1a::bb:10.2.0.0/112

(112 being /128 minuse the /96 v6 prefix length)

Then people in my tailnet can:

     $ curl '[fd7a:115c:a1e0:b1a::bb:10.2.0.230]'
     <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ....

Updates #3616, etc

RELNOTE=initial support for TS IPv6 addresses to route v4 "via" specific nodes

Change-Id: I9b49b6ad10410a24b5866b9fbc69d3cae1f600ef
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-11 17:26:07 -07:00
David Eger
f992749b98 cmd/tailscale: Add file get --loop flag.
To "automatically receive taildrop files to my Downloads directory,"
user currently has to run 'tailscale file get' in a loop.  Make
it easy to do this without shell.

Updates: #2312

Signed-off-by: David Eger <david.eger@gmail.com>
2022-04-11 17:23:08 -07:00
James Tucker
f4aad61e67 wgengine/monitor: ignore duplicate RTM_NEWADDRs
Ignoring the events at this layer is the simpler path for right now, a
broader change should follow to suppress irrelevant change events in a
higher layer so as to avoid related problems with other monitoring paths
on other platforms.  This approach may also carry a small risk that it
applies an at-most-once invariant low in the chain that could be assumed
otherwise higher in the code.

I adjusted the newAddrMessage type to include interface index rather
than a label, as labels are not always supplied, and in particular on my
test hosts they were consistently missing for ipv6 address messages.

I adjusted the newAddrMessage.Addr field to be populated from
Attributes.Address rather than Attributes.Local, as again for ipv6
.Local was always empty, and with ipv4 the .Address and .Local contained
the same contents in each of my test environments.

Update #4282

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-11 14:35:19 -07:00
James Tucker
2f69c383a5 wgengine/monitor: add envknob TS_DEBUG_NETLINK
While I trust the test behavior, I also want to assert the behavior in a
reproduction environment, this envknob gives me the log information I
need to do so.

Update #4282

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-11 14:35:19 -07:00
Tom DNetto
8f6d8cf979 tstest/integration/vms: test on stable nixos (21.11)
I would like to do some more customized integration tests in the future,
(specifically, bringing up a mitm proxy and testing tailscaled through that)
so hoping to bring back the nixos wiring to support that.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-11 12:54:56 -07:00
James Tucker
8226f1482c go.mod: bump rtnetlink for address label encoding (#4386)
This will enable me to land tests for the upcoming monitor change in
PR #4385.

Update #4385
Update #4282

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-08 14:17:53 -07:00
Tom DNetto
f923ce6f87 shell.nix: use tailscale-go for compilation
This change builds a derivation for tailscale-go and makes it available in the
users development environment. This is consistent with the shell.nix in corp/.

Once go1.18 is in a stable Nixpkgs release we can avoid relying on derivations
from nixpkgs head. For now, this works well, and the fetched derivations are
cached in the Nix store according to the usual rules.

Fixes #4231

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-08 14:10:50 -07:00
Tom
24bdcbe5c7 net/dns, net/dns/resolver, wgengine: refactor DNS request path (#4364)
* net/dns, net/dns/resolver, wgengine: refactor DNS request path

Previously, method calls into the DNS manager/resolver types handled DNS
requests rather than DNS packets. This is fine for UDP as one packet
corresponds to one request or response, however will not suit an
implementation that supports DNS over TCP.

To support PRs implementing this in the future, wgengine delegates
all handling/construction of packets to the magic DNS endpoint, to
the DNS types themselves. Handling IP packets at this level enables
future support for both UDP and TCP.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-08 12:17:31 -07:00
Tom DNetto
3b3d1b9350 tstest/integration/vms: consistently use two dashes for command-line switches
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-08 09:54:22 -07:00
Xiaochao Dong (@damnever)
7d97800d52 cmd/tailscale: make web mode preserve URL scheme in Synology redirect
Signed-off-by: Xiaochao Dong (@damnever) <the.xcdong@gmail.com>
2022-04-08 06:18:13 -07:00
James Tucker
2550acfd9d go.mod: bump netstack for clone reset fix (#4379)
In tracking down issue #4144 and reading through the netstack code in
detail, I discovered that the packet buf Clone path did not reset the
packetbuf it was getting from the sync.Pool. The fix was sent upstream
https://github.com/google/gvisor/pull/7385, and this bump pulls that in.
At this time there is no known path that this fixes, however at the time
of upstream submission this reset at least one field that could lead to
incorrect packet routing if exercised, a situation that could therefore
lead to an information leak.

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-07 19:03:18 -07:00
David Anderson
f570372b4d control/controlbase: don't enforce a max protocol version at handshake time.
Doing so makes development unpleasant, because we have to first break the
client by bumping to a version the control server rejects, then upgrade
the control server to make it accept the new version.

This strict rejection at handshake time is only necessary if we want to
blocklist some vulnerable protocol versions in the future. So, switch
to a default-permissive stance: until we have such a version that we
have to eagerly block early, we'll accept whatever version the client
presents, and leave it to the user of controlbase.Conn to make decisions
based on that version.

Noise still enforces that the client and server *agree* on what protocol
version is being used, and the control server still has the option to
finish the handshake and then hang up with an in-noise error, rather
than abort at the handshake level.

Updates #3488

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-04-07 17:55:29 -07:00
James Tucker
c6ac29bcc4 wgengine/netstack: disable refsvfs2 leak tracking (#4378)
In addition an envknob (TS_DEBUG_NETSTACK_LEAK_MODE) now provides access
to set leak tracking to more useful values.

Fixes #4309

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-07 17:21:45 -07:00
Tom DNetto
858ab80172 tstest/integration/vms: fix docs, qemu-img invocation
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-07 15:40:14 -07:00
Xe Iaso
55161b3d92 cmd/mkpkg: use package flag (#4373)
Also removes getopt

Signed-off-by: Xe <xe@tailscale.com>
2022-04-07 16:38:33 -04:00
David Anderson
02ad987e24 control/controlbase: make the protocol version number selectable.
This is so that we can plumb our client capability version through
the protocol as the Noise version. The capability version increments
more frequently than strictly required (the Noise version only needs
to change when cryptographically-significant changes are made to
the protocol, whereas the capability version also indicates changes
in non-cryptographically-significant parts of the protocol), but this
gives us a safe pre-auth way to determine if the client supports
future protocol features, while still relying on Noise's strong
assurance that the client and server have agreed on the same version.

Currently, the server executes the same protocol regardless of the
version number, and just presents the version to the caller so they
can do capability-based things in the upper RPC protocol. In future,
we may add a ratchet to disallow obsolete protocols, or vary the
Noise handshake behavior based on requested version.

Updates #3488

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-04-07 13:25:28 -07:00
Xe Iaso
be861797b4 cmd/mkpkg: add name argument (#4372)
* shell.nix: rename goimports to gotools

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

* cmd/mkpkg: allow specifying description and name in flag args

Signed-off-by: Xe <xe@tailscale.com>
2022-04-07 16:05:04 -04:00
Ramya Nagarajan
e014b4d970 api: update acl/validate data format (#4366)
Expected input is JSON formatted []policy.ACLTest

Signed-off-by: Ramya Nagarajan <ramya@tailscale.com>
2022-04-07 10:33:52 -07:00
Matt Layher
c79c72c4fc go.mod: github.com/mdlayher/sdnotify@v1.0.0
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2022-04-06 19:25:50 -07:00
Tom
6be7931eb4 net/dns/resolver: return symbolic PTR records for TS service IPs (#4361)
Fixes #1233

Signed-off-by: Tom DNetto <tom@tailscale.com>

Co-authored-by: Tom DNetto <tom@tailscale.com>
2022-04-06 15:56:21 -07:00
oliverpool
0b273e1857 cmd/tailscale: drop special exit code 125 for gokrazy
No needed since gokrazy doesn't restart successful processes anymore: https://github.com/gokrazy/gokrazy/pull/127

Signed-off-by: Olivier Charvin <git@olivier.pfad.fr>
2022-04-06 06:21:44 -07:00
Maisem Ali
3603a18710 ipn/localapi: add endpoint to request id token
Updates tailscale/corp#4347

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-05 14:48:45 -07:00
Maisem Ali
035e8ab00e tailcfg: add Token{Request,Response} types
Updates tailscale/corp#4347

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-05 14:48:45 -07:00
Joe Tsai
01adcfa688 tailcfg: add omitempty to all fields of Hostinfo (#4360)
This reduces the noise when marshaling only a subset of this type.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-04-05 13:25:14 -07:00
Maisem Ali
ac2033d98c go.mod: bump staticcheck (#4359)
Updates #4194

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-05 12:24:33 -07:00
Maisem Ali
9f604f2bd3 derp: add (*Server).IsClientConnectedForTest func. (#4331)
This allows tests to verfiy that a DERP connection was actually
established.

Related to #4326
Updates tailscale/corp#2579

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-30 10:50:50 -07:00
Brad Fitzpatrick
3d180c0376 go.mod, ssh/tailssh, tempfork/gliderlabs: bump x/crypto/ssh fork for NoClientAuthCallback
Prep for evaluating SSHPolicy earlier to decide whether certs are
required, which requires knowing the target SSH user.

Updates #3802

Change-Id: I2753ec8069e7f19c9121300d0fb0813c1c627c36
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-29 18:17:34 -07:00
Maisem Ali
f4686a76a1 envknob: use the correct key when logging (#4319) 2022-03-29 14:04:12 -07:00
Maisem Ali
309ddef852 net/netutil: add CheckIPForwardingLinux (#4301)
Combine the code between `LocalBackend.CheckIPForwarding` and
`controlclient.ipForwardingBroken`.

Fixes #4300

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-28 10:24:11 -07:00
David Anderson
7c7f37342f prober: used keyed initializer for LimitedReader.
Reported by go vet.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-03-26 21:45:27 -07:00
Maisem Ali
909f40da84 util/groupmember: remove redundant code (#4298)
Now that we have 30faf968b1
this is no longer needed.

Fixes #3001

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-26 21:45:06 -07:00
Brad Fitzpatrick
e4d8d5e78b net/packet, wgengine/netstack: remove workaround for old gvisor ECN bug
Fixes #2642

Change-Id: Ic02251d24a4109679645d1c8336e0f961d0cce13
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-26 21:24:24 -07:00
Brad Fitzpatrick
5a44f9f5b5 tempfork: temporarily fork gliderlabs/ssh and x/crypto/ssh
While we rearrange/upstream things.

gliderlabs/ssh is forked into tempfork from our prior fork
at be8b7add40

x/crypto/ssh OTOH is forked at
https://github.com/tailscale/golang-x-crypto because it was gnarlier
to vendor with various internal packages, etc.
Its git history shows where it starts (2c7772ba30643b7a2026cbea938420dce7c6384d).

Updates #3802

Change-Id: I546e5cdf831cfc030a6c42557c0ad2c58766c65f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-26 21:07:01 -07:00
Maisem Ali
6fecc16c3b ipn/ipnlocal: do not process old status messages received out of order
When `setWgengineStatus` is invoked concurrently from multiple
goroutines, it is possible that the call invoked with a newer status is
processed before a call with an older status. e.g. a status that has
endpoints might be followed by a status without endpoints. This causes
unnecessary work in the engine and can result in packet loss.

This patch adds an `AsOf time.Time` field to the status to specifiy when the
status was calculated, which later allows `setWgengineStatus` to ignore
any status messages it receives that are older than the one it has
already processed.

Updates tailscale/corp#2579

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-26 20:23:50 -07:00
Brad Fitzpatrick
753f1bfad4 cmd/tailscale: write fewer known_hosts, resolve ssh host to FQDN early
Updates #3802

Change-Id: Ic44fa2e6661a9c046e725c04fa6b8213d3d4d2b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-25 15:17:27 -07:00
Brad Fitzpatrick
df93158aac cmd/tailscale: generate known_hosts file for 'tailscale ssh'
Updates #3802

Change-Id: I7a0052392f000ee44fc8e719f6666756aab91f3d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-25 14:10:48 -07:00
Brad Fitzpatrick
cceacda5eb ipn/ipnstate: put SSH Host Keys in ipnstate.PeerStatus
Updates #3802

Change-Id: I47ba3b4545b25988f375bd867aecd98bb0da8d79
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-25 12:14:39 -07:00
Maisem Ali
42ee4c917d go.toolchain.rev: pick up httptest race fix
tailscale/go@5ce3ec4d89

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-25 09:53:10 -07:00
Brad Fitzpatrick
a0e345dba4 tsnet: set Hostinfo.Package to "tsnet" on use
Fixes #4256

Change-Id: I11276596c3724bead1e0aa1f2e57ba75cef3b6be
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-25 09:26:02 -07:00
Maisem Ali
b2f269d5b7 net/dnsfallback: do not attempt lookups of IPs.
Currently if the passed in host is an IP, Lookup still attempts to
resolve it with a dns server. This makes it just return the IP directly.

Updates tailscale/corp#4475

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-24 22:43:01 -07:00
Maisem Ali
b45bb577a0 net/dnscache: do not call LookupIPFallback if the context was canceled.
When the context is canceled, dc.dialOne returns an error from line 345.
This causes the defer on line 312 to try to resolve the host again, which
triggers a dns lookup of "127.0.0.1" from derp.

Updates tailscale/corp#4475

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-03-24 22:43:01 -07:00
Brad Fitzpatrick
8294915780 cmd/tailscale/cli: add start of 'ssh' subcommand
Updates #3802

Change-Id: Iabc07c00c7e4f43944cfe7daec8d2b66ac002289
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-24 21:43:42 -07:00
Brad Fitzpatrick
06fcf3b225 cmd/tailscale: make status --peers=false work earlier + in JSON mode
And return an error if you use non-flag arguments.

Change-Id: I0dd6c357eb5cabd0f17020f21ba86406aea21681
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-24 14:02:22 -07:00
Brad Fitzpatrick
5df12b9059 client/tailscale, cmd/tailscale, localapi: add 'tailscale nc' (actually)
Adds missing file from fc12cbfcd3.

GitHub was having issues earlier and it was all green because the
checks never actually ran, but the DCO non-Actions check at least did,
so "green" and I merged, not realizing it hadn't really run anything.

Updates #3802

Change-Id: I29f605eebe5336f1f3ca28ebb78b092dd99d9fd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-24 11:36:34 -07:00
Brad Fitzpatrick
fc12cbfcd3 client/tailscale, cmd/tailscale, localapi: add 'tailscale nc'
This adds a "tailscale nc" command that acts a bit like "nc", but
dials out via tailscaled via localapi.

This is a step towards a "tailscale ssh", as we'll use "tailscale nc"
as a ProxyCommand for in some cases (notably in userspace mode).

But this is also just useful for debugging & scripting.

Updates #3802

RELNOTE=tailscale nc

Change-Id: Ia5c37af2d51dd0259d5833d80264d3ad5f68446a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-24 10:28:53 -07:00
Brad Fitzpatrick
b647977b33 net/netutil: move some net utils from control/controlhttp to netutil
In prep for reuse elsewhere.

Change-Id: I1b804edf76ac66b9108e6f434e77eab7a7472d69
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-24 09:56:10 -07:00
Brad Fitzpatrick
d2f3ec8a63 envknob, ipn/ipnlocal: add SSH admin kill switch
Updates #3802

Change-Id: I6127907446d1a6be1b097d9ba3b534f2b8eb707f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-23 15:40:30 -07:00
Josh Bleecher Snyder
77b4fe0afa all: remove "no 1.18 support" failures
We have worked around the issue in DERP,
so the vanilla Go 1.18 toolchain now works.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2022-03-23 13:04:48 -07:00
Brad Fitzpatrick
98984c1a9a cmd/proxy-to-grafana: fix package doc code snippet
Markdown isn't supported.

Change-Id: I8d9bb92260c164dc277afbce624f64fc2faf5125
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-23 12:43:41 -07:00
Nick O'Neill
5fa502b5dc cmd/proxy-to-grafana: use grafana's authproxy to log in tailnet users (#4208)
Signed-off-by: Nick O'Neill <nick@tailscale.com>
2022-03-23 11:33:53 -07:00
Brad Fitzpatrick
4fc38888d2 go.mod: bump x/crypto for SSH change
(for golang/go#51808)

Updates #3802

Change-Id: Ifbd483c0144b4c86da69143b23b2a06da7672c92
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-23 10:22:16 -07:00
Brad Fitzpatrick
8144c6d87d tsnet: fail with nice message with go1.18 on darwin
Like we did in ead16b24ec for tailscaled.

Updates #4258
Due to golang/go#51759

Change-Id: I6effcea7c5f2ec264b9711f4c316f8fca09490f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-22 20:43:09 -07:00
Brad Fitzpatrick
0861923c21 ssh/tailssh, tailcfg: add more HoldAndDelegate expansions, document
Updates #3802

Change-Id: I447f06b49e2a917bffe36881d0634c9195085512
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-22 17:56:45 -07:00
dependabot[bot]
a121b9f263 .github: Bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 16:07:00 -07:00
Brad Fitzpatrick
091ea4a4a5 ssh/tailssh: support placeholders in SSHAction.HoldAndDelegate URL
Updates #3802

Change-Id: I60f9827409d14fd4f4824d102ba11db49bf0d365
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-22 16:02:40 -07:00
dependabot[bot]
257d75beb1 .github: Bump actions/setup-go from 2.1.5 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.5 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.5...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 15:58:46 -07:00
dependabot[bot]
f2b0faf91e .github: Bump actions/upload-artifact from 2.3.1 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.3.1 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.3.1...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 15:57:32 -07:00
dependabot[bot]
7fbb6a76ad .github: Bump actions/cache from 2 to 3
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 15:55:50 -07:00
Dave Anderson
0968b2d55a prober: support adding key/value labels to probes. (#4250)
prober: add labels to Probe instances.

This allows especially dynamically-registered probes to have a bunch
more dimensions along which they can be sliced in Prometheus.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-03-22 13:45:11 -07:00
Brad Fitzpatrick
f3b13604b3 control/controlclient, ipn/ipnlocal, tailcfg: add MapResponse.PopBrowserURL
Updates #3802

Change-Id: I89481fc5782a0cc8084354706f8f28d94f197325
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-22 09:37:00 -07:00
Brad Fitzpatrick
3ea6ddbb5f control/controlbase: use less memory when idle (remove rxState.buf)
Uses 4KB less per Conn.

Fixes #4113

Change-Id: I38d25b1cf5ecf45423404a330b7ecab79a9fb176
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-21 17:47:50 -07:00
James Tucker
445c04c938 wgengine: inject packetbuffers rather than bytes (#4220)
Plumb the outbound injection path to allow passing netstack
PacketBuffers down to the tun Read, where they are decref'd to enable
buffer re-use. This removes one packet alloc & copy, and reduces GC
pressure by pooling outbound injected packets.

Fixes #2741
Signed-off-by: James Tucker <james@tailscale.com>
2022-03-21 14:58:43 -07:00
David Anderson
a09c30aac2 prober: refactor probe state into a Probe struct.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-03-21 13:45:08 -07:00
David Anderson
94aaec5c66 prober: rename Probe to ProbeFunc.
Making way for a future Probe struct to encapsulate per-probe state.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-03-21 13:45:08 -07:00
David Anderson
7b4960316b tsweb: add PrometheusVar, for vars that want to output varz themselves.
This enables the infrequent use of more complex Prometheus types, such as
timeseries with high/irregular label cardinality, without needing to
discover and implement generic abstracted type like LabelMap for each one.

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-03-21 12:33:18 -07:00
Brad Fitzpatrick
f6642e0ece Makefile: add tidy target
Change-Id: I6cd3a6837f9ddfd283f57378465dcefa4d0ada47
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-21 11:06:13 -07:00
James Tucker
73314009d0 go.mod: bump netstack (#4222)
Primarily this is for f375784d83852b1e3ff20cc9de0648b3c0cf8525 and the
related commits that provide buffer pooling for the endpoint code paths
we use.

Signed-off-by: James Tucker <james@tailscale.com>
2022-03-21 11:01:40 -07:00
Brad Fitzpatrick
f7e976db55 tailcfg, ssh/tailssh: make SSHUser value '=' map ssh-user to same local-user
Updates #3802

Change-Id: Icde60d4150ca15c25d615a4effb3d3c236f020a8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-21 10:49:37 -07:00
Brad Fitzpatrick
21445b56a5 control/controlbase: use less memory when idle (remove txState.buf)
Uses 4KB less per Conn.

Updates #4113

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-03-20 16:35:43 -07:00
305 changed files with 17004 additions and 5241 deletions

View File

@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: macOS build cmd
env:

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: FreeBSD build cmd
env:

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: OpenBSD build cmd
env:

47
.github/workflows/cross-wasm.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Wasm-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Wasm build CLI and client modules
env:
GOOS: js
GOARCH: wasm
run: go build ./cmd/tailscale/cli ./ipn/... ./net/... ./safesocket ./types/... ./wgengine/...
- 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

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Windows build cmd
env:

View File

@@ -14,12 +14,12 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: depaware tailscaled
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env sh
#
# This is a temporary hack to work around
# https://github.com/golang/go/issues/51629 , wherein the stringer
# generator doesn't work with generics.
#
# This script is the equivalent of `go generate ./...`, except that it
# only runs generate on packages that don't try to use stringer.
set -e
find . -name '*.go' | xargs grep -l go:generate | xargs -n1 dirname | sort -u | while read dir; do
if ! egrep "cmd/(stringer|cloner)" $dir/*.go; then
set -x
go generate -tags=hermetic $dir
set +x
fi
done

View File

@@ -15,23 +15,24 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: check 'go generate' is clean
# The shell script invocation below is a temporary hack for
# https://github.com/tailscale/tailscale/issues/4194. When
# that issue is fixed, replace its invocation with:
# go generate --tags=hermetic ./...
run: |
set -e
./.github/workflows/go-generate-without-stringer.sh
if [[ "${{github.ref}}" == release-branch/* ]]
then
pkgs=$(go list ./... | grep -v dnsfallback)
else
pkgs=$(go list ./... | grep -v dnsfallback)
fi
go generate $pkgs
echo
echo
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)

View File

@@ -14,12 +14,12 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Run license checker
run: ./scripts/check_license_headers.sh .

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Basic build
run: go build ./cmd/...

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Basic build
run: go build ./cmd/...

View File

@@ -17,13 +17,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Basic build
run: GOARCH=386 go build ./cmd/...

View File

@@ -14,12 +14,12 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Run go vet
run: go vet ./...

View File

@@ -16,12 +16,12 @@ jobs:
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: Set up Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Checkout Code
uses: actions/checkout@v1
uses: actions/checkout@v3
- name: Run VM tests
run: go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004

View File

@@ -17,15 +17,15 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
# Note: unlike some other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache

View File

@@ -17,15 +17,15 @@ jobs:
steps:
- name: Install Go
uses: actions/setup-go@v2.1.5
uses: actions/setup-go@v3
with:
go-version: 1.18.x
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
# Note: unlike some other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache

View File

@@ -8,6 +8,9 @@ usage:
vet:
./tool/go vet ./...
tidy:
./tool/go mod tidy -compat=1.17
updatedeps:
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
./tool/go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale

View File

@@ -1 +1 @@
1.23.0
1.26.0

151
api.md
View File

@@ -22,9 +22,9 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
* **[Tailnets](#tailnet)**
- ACLs
- [GET tailnet ACL](#tailnet-acl-get)
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
- [POST tailnet ACL](#tailnet-acl-post)
- [POST tailnet ACL preview](#tailnet-acl-preview-post)
- [POST tailnet ACL validate](#tailnet-acl-validate-post)
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [Keys](#tailnet-keys)
@@ -42,12 +42,12 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
## Device
<!-- TODO: description about what devices are -->
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
To find the deviceID of a particular device, you can use the ["GET /devices"](#getdevices) API call and generate a list of devices on your network.
To find the deviceID of a particular device, you can use the ["GET /devices"](#getdevices) API call and generate a list of devices on your network.
Find the device you're looking for and get the "id" field.
This is your deviceID.
This is your deviceID.
<a name=device-get></a>
@@ -60,7 +60,7 @@ Use the `fields` query parameter to explicitly indicate which fields are returne
##### Parameters
##### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
Currently, supported options are:
* `all`: returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
@@ -72,7 +72,7 @@ If more than one option is indicated, then the union is used.
For example, for `fields=default,all`, all fields are returned.
If the `fields` parameter is not provided, then the default option is used.
##### Example
##### Example
```
GET /api/v2/device/12345
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
@@ -102,10 +102,10 @@ Response
"nodeKey":"nodekey:user1-node-key",
"blocksIncomingConnections":false,
"enabledRoutes":[
],
"advertisedRoutes":[
],
"clientConnectivity": {
"endpoints":[
@@ -141,7 +141,7 @@ Response
<a name=device-delete></a>
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
Deletes the provided device from its tailnet.
Deletes the provided device from its tailnet.
The device must belong to the user's tailnet.
Deleting shared/external devices is not supported.
Supply the device of interest in the path using its ID.
@@ -159,7 +159,7 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
Response
If successful, the response should be empty:
If successful, the response should be empty:
```
< HTTP/1.1 200 OK
...
@@ -167,7 +167,7 @@ If successful, the response should be empty:
* Closing connection 0
```
If the device is not owned by your tailnet:
If the device is not owned by your tailnet:
```
< HTTP/1.1 501 Not Implemented
...
@@ -317,14 +317,9 @@ Allows for updating properties on the device key.
- Provide `false` to enable the device's key expiry. Sets the key to expire at the original expiry time prior to disabling. The key may already have expired. In that case, the device must be re-authenticated.
- Empty value will not change the key expiry.
`preauthorized`
- If `true`, don't require machine authorization (if enabled on the tailnet)
```
{
"keyExpiryDisabled": true,
"preauthorized": true
"keyExpiryDisabled": true
}
```
@@ -339,8 +334,8 @@ curl 'https://api.tailscale.com/api/v2/device/11055/key' \
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.
## 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.
@@ -620,7 +615,12 @@ Response:
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
This endpoint works in one of two modes:
1. with a request body that's a JSON array, the body is interpreted as ACL tests to run against the domain's current ACLs.
2. with a request body that's a JSON object, the body is interpreted as a hypothetical new JSON (HuJSON) body with new ACLs, including any tests.
In either case, this endpoint does not modify the ACL in any way.
##### Parameters
@@ -630,24 +630,39 @@ The POST body should be a JSON formatted array of ACL Tests.
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
##### Example
##### Example with tests
```
POST /api/v2/tailnet/example.com/acl/validate
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"]}
]
}'
]'
```
##### Example with an ACL body
```
POST /api/v2/tailnet/example.com/acl/validate
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:*"] },
],
"Tests", [
{"src": "100.105.106.107", "allow": ["1.2.3.4:80"]}
],
}'
```
Response:
If all the tests pass, the response will be empty, with an http status code of 200.
Failed test error response:
A 400 http status code and the errors in the response body.
The HTTP status code will be 200 if the request was well formed and there were no server errors, even in the case of failing tests or an invalid ACL. Look at the response body to determine whether there was a problem within your ACL or tests.
If there's a problem, the response body will be a JSON object with a non-empty `message` property and optionally additional details in `data`:
```
{
"message":"test(s) failed",
@@ -660,6 +675,8 @@ A 400 http status code and the errors in the response body.
}
```
An empty body or a JSON object with no `message` is returned on success.
<a name=tailnet-devices></a>
### Devices
@@ -667,7 +684,7 @@ A 400 http status code and the errors in the response body.
<a name=tailnet-devices-get></a>
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
Lists the devices in a tailnet.
Lists the devices in a tailnet.
Supply the tailnet of interest in the path.
Use the `fields` query parameter to explicitly indicate which fields are returned.
@@ -676,7 +693,7 @@ Use the `fields` query parameter to explicitly indicate which fields are returne
###### Query Parameters
`fields` - Controls which fields will be included in the returned response.
Currently, supported options are:
Currently, supported options are:
* `all`: Returns all fields in the response.
* `default`: return all fields except:
* `enabledRoutes`
@@ -696,7 +713,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
-u "tskey-yourapikey123:"
```
Response
Response
```
{
"devices":[
@@ -798,11 +815,15 @@ Supply the tailnet in the path.
`capabilities` - A mapping of resources to permissible actions.
```
{
"capabilities": {
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
"ephemeral": false,
"preauthorized": false,
"tags": [
"tag:example"
]
}
}
}
@@ -822,11 +843,13 @@ The full key can no longer be retrieved by the server.
```
echo '{
"capabilities": {
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": false
"ephemeral": false,
"preauthorized": false,
"tags": [ "tag:example" ]
}
}
}
@@ -842,7 +865,7 @@ Response:
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
"created": "2021-12-09T23:22:39Z",
"expires": "2022-03-09T23:22:39Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false, "preauthorized": false, "tags": [ "tag:example" ]}}}
}
```
@@ -872,10 +895,22 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
Response:
```
{
"id": "k123456CNTRL",
"created": "2021-12-09T22:13:53Z",
"expires": "2022-03-09T22:13:53Z",
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
"id": "k123456CNTRL",
"created": "2022-05-05T18:55:44Z",
"expires": "2022-08-03T18:55:44Z",
"capabilities": {
"devices": {
"create": {
"reusable": false,
"ephemeral": true,
"preauthorized": false,
"tags": [
"tag:bar",
"tag:foo"
]
}
}
}
}
```
@@ -906,13 +941,13 @@ curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k12345
<a name=tailnet-dns-nameservers-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
Lists the DNS nameservers for a tailnet.
Lists the DNS nameservers for a tailnet.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
##### Example
##### Example
```
GET /api/v2/tailnet/example.com/dns/nameservers
@@ -920,7 +955,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
-u "tskey-yourapikey123:"
```
Response
Response
```
{
"dns": ["8.8.8.8"],
@@ -930,7 +965,7 @@ Response
<a name=tailnet-dns-nameservers-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
Supply the tailnet of interest in the path.
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
@@ -944,7 +979,7 @@ Note that changing the list of DNS nameservers may also affect the status of Mag
```
##### Returns
Returns the new list of nameservers and the status of MagicDNS.
Returns the new list of nameservers and the status of MagicDNS.
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
@@ -988,31 +1023,31 @@ Retrieves the DNS preferences that are currently set for the given tailnet.
Supply the tailnet of interest in the path.
##### Parameters
No parameters.
No parameters.
##### Example
```
GET /api/v2/tailnet/example.com/dns/preferences
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
-u "tskey-yourapikey123:"
-u "tskey-yourapikey123:"
```
Response:
```
{
"magicDNS":false,
"magicDNS":false,
}
```
<a name=tailnet-dns-preferences-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
Note that MagicDNS is dependent on DNS servers.
Note that MagicDNS is dependent on DNS servers.
If there is at least one DNS server, then MagicDNS can be enabled.
If there is at least one DNS server, then MagicDNS can be enabled.
Otherwise, it returns an error.
Note that removing all nameservers will turn off MagicDNS.
Note that removing all nameservers will turn off MagicDNS.
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
##### Parameters
@@ -1043,7 +1078,7 @@ If there are no DNS servers, it returns an error message:
}
```
If there are DNS servers:
If there are DNS servers:
```
{
"magicDNS":true,
@@ -1052,8 +1087,8 @@ If there are DNS servers:
<a name=tailnet-dns-searchpaths-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
Retrieves the list of search paths that is currently set for the given tailnet.
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
Retrieves the list of search paths that is currently set for the given tailnet.
Supply the tailnet of interest in the path.
@@ -1064,7 +1099,7 @@ No parameters.
```
GET /api/v2/tailnet/example.com/dns/searchpaths
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
-u "tskey-yourapikey123:"
-u "tskey-yourapikey123:"
```
Response:
@@ -1076,7 +1111,7 @@ Response:
<a name=tailnet-dns-searchpaths-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
##### Parameters

View File

@@ -32,7 +32,7 @@ REPOS="${REPOS:-${DEFAULT_REPOS}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
go run github.com/tailscale/mkctr@latest \
go run github.com/tailscale/mkctr \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
@@ -40,7 +40,9 @@ go run github.com/tailscale/mkctr@latest \
-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}"
--push="${PUSH}" \
/bin/sh /tailscale/run.sh

469
client/tailscale/acl.go Normal file
View File

@@ -0,0 +1,469 @@
// 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.18
// +build go1.18
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"inet.af/netaddr"
)
// ACLRow defines a rule that grants access by a set of users or groups to a set of servers and ports.
type ACLRow struct {
Action string `json:"action,omitempty"` // valid values: "accept"
Users []string `json:"users,omitempty"`
Ports []string `json:"ports,omitempty"`
}
// ACLTest defines a test for your ACLs to prevent accidental exposure or revoking of access to key servers and ports.
type ACLTest struct {
User string `json:"user,omitempty"` // source
Allow []string `json:"allow,omitempty"` // expected destination ip:port that user can access
Deny []string `json:"deny,omitempty"` // expected destination ip:port that user cannot access
}
// ACLDetails contains all the details for an ACL.
type ACLDetails struct {
Tests []ACLTest `json:"tests,omitempty"`
ACLs []ACLRow `json:"acls,omitempty"`
Groups map[string][]string `json:"groups,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty"`
Hosts map[string]string `json:"hosts,omitempty"`
}
// ACL contains an ACLDetails and metadata.
type ACL struct {
ACL ACLDetails
ETag string // to check with version on server
}
// ACLHuJSON contains the HuJSON string of the ACL and metadata.
type ACLHuJSON struct {
ACL string
Warnings []string
ETag string // to check with version on server
}
// ACL makes a call to the Tailscale server to get a JSON-parsed version of the ACL.
// The JSON-parsed version of the ACL contains no comments as proper JSON does not support
// comments.
func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.ACL: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
// Otherwise, try to decode the response.
var aclDetails ACLDetails
if err = json.Unmarshal(b, &aclDetails); err != nil {
return nil, err
}
acl = &ACL{
ACL: aclDetails,
ETag: resp.Header.Get("ETag"),
}
return acl, nil
}
// ACLHuJSON makes a call to the Tailscale server to get the ACL HuJSON and returns
// it as a string.
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
// changes are allowing comments and trailing comments. See the following links for more info:
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
// https://github.com/tailscale/hujson
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.ACLHuJSON: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/hujson")
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
data := struct {
ACL []byte `json:"acl"`
Warnings []string `json:"warnings"`
}{}
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
}
acl = &ACLHuJSON{
ACL: string(data.ACL),
Warnings: data.Warnings,
ETag: resp.Header.Get("ETag"),
}
return acl, nil
}
// ACLTestFailureSummary specifies a user for which ACL tests
// failed and the related user-friendly error messages.
//
// ACLTestFailureSummary specifies the JSON format sent to the
// JavaScript client to be rendered in the HTML.
type ACLTestFailureSummary struct {
User string `json:"user"`
Errors []string `json:"errors"`
}
// ACLTestError is ErrResponse but with an extra field to account for ACLTestFailureSummary.
type ACLTestError struct {
ErrResponse
Data []ACLTestFailureSummary `json:"data"`
}
func (e ACLTestError) Error() string {
return fmt.Sprintf("%s, Data: %+v", e.ErrResponse.Error(), e.Data)
}
func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, "", err
}
if avoidCollisions {
req.Header.Set("If-Match", etag)
}
req.Header.Set("Accept", acceptHeader)
req.Header.Set("Content-Type", "application/hujson")
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, "", err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
// check if test error
var ate ACLTestError
if err := json.Unmarshal(b, &ate); err != nil {
return nil, "", err
}
ate.Status = resp.StatusCode
return nil, "", ate
}
return b, resp.Header.Get("ETag"), nil
}
// SetACL sends a POST request to update the ACL according to the provided ACL object. If
// `avoidCollisions` is true, it will use the ETag obtained in the GET request in an If-Match
// header to check if the previously obtained ACL was the latest version and that no updates
// were missed.
//
// Returns error with status code 412 if mistmached ETag and avoidCollisions is set to true.
// Returns error if ACL has tests that fail.
// Returns error if there are other errors with the ACL.
func (c *Client) SetACL(ctx context.Context, acl ACL, avoidCollisions bool) (res *ACL, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetACL: %w", err)
}
}()
postData, err := json.Marshal(acl.ACL)
if err != nil {
return nil, err
}
b, etag, err := c.aclPOSTRequest(ctx, postData, avoidCollisions, acl.ETag, "application/json")
if err != nil {
return nil, err
}
// Otherwise, try to decode the response.
var aclDetails ACLDetails
if err = json.Unmarshal(b, &aclDetails); err != nil {
return nil, err
}
res = &ACL{
ACL: aclDetails,
ETag: etag,
}
return res, nil
}
// SetACLHuJSON sends a POST request to update the ACL according to the provided ACL object. If
// `avoidCollisions` is true, it will use the ETag obtained in the GET request in an If-Match
// header to check if the previously obtained ACL was the latest version and that no updates
// were missed.
//
// Returns error with status code 412 if mistmached ETag and avoidCollisions is set to true.
// Returns error if the HuJSON is invalid.
// Returns error if ACL has tests that fail.
// Returns error if there are other errors with the ACL.
func (c *Client) SetACLHuJSON(ctx context.Context, acl ACLHuJSON, avoidCollisions bool) (res *ACLHuJSON, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetACLHuJSON: %w", err)
}
}()
postData := []byte(acl.ACL)
b, etag, err := c.aclPOSTRequest(ctx, postData, avoidCollisions, acl.ETag, "application/hujson")
if err != nil {
return nil, err
}
res = &ACLHuJSON{
ACL: string(b),
ETag: etag,
}
return res, nil
}
// UserRuleMatch specifies the source users/groups/hosts that a rule targets
// and the destination ports that they can access.
// LineNumber is only useful for requests provided in HuJSON form.
// While JSON requests will have LineNumber, the value is not useful.
type UserRuleMatch struct {
Users []string `json:"users"`
Ports []string `json:"ports"`
LineNumber int `json:"lineNumber"`
}
// ACLPreviewResponse is the response type of previewACLPostRequest
type ACLPreviewResponse struct {
Matches []UserRuleMatch `json:"matches"` // ACL rules that match the specified user or ipport.
Type string `json:"type"` // The request type: currently only "user" or "ipport".
PreviewFor string `json:"previewFor"` // A specific user or ipport.
}
// ACLPreview is the response type of PreviewACLForUser, PreviewACLForIPPort, PreviewACLHuJSONForUser, and PreviewACLHuJSONForIPPort
type ACLPreview struct {
Matches []UserRuleMatch `json:"matches"`
User string `json:"user,omitempty"` // Filled if response of PreviewACLForUser or PreviewACLHuJSONForUser
IPPort string `json:"ipport,omitempty"` // Filled if response of PreviewACLForIPPort or PreviewACLHuJSONForIPPort
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("type", previewType)
q.Add("previewFor", previewFor)
req.URL.RawQuery = q.Encode()
req.Header.Set("Content-Type", "application/hujson")
c.setAuth(req)
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
if err = json.Unmarshal(b, &res); err != nil {
return nil, err
}
return res, nil
}
// PreviewACLForUser determines what rules match a given ACL for a user.
// The ACL can be a locally modified or clean ACL obtained from server.
//
// Returns ACLPreview on success with matches in a slice. If there are no matches,
// the call is still successful but Matches will be an empty slice.
// Returns error if the provided ACL is invalid.
func (c *Client) PreviewACLForUser(ctx context.Context, acl ACL, user string) (res *ACLPreview, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.PreviewACLForUser: %w", err)
}
}()
postData, err := json.Marshal(acl.ACL)
if err != nil {
return nil, err
}
b, err := c.previewACLPostRequest(ctx, postData, "user", user)
if err != nil {
return nil, err
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
}, nil
}
// PreviewACLForIPPort determines what rules match a given ACL for a ipport.
// The ACL can be a locally modified or clean ACL obtained from server.
//
// Returns ACLPreview on success with matches in a slice. If there are no matches,
// the call is still successful but Matches will be an empty slice.
// Returns error if the provided ACL is invalid.
func (c *Client) PreviewACLForIPPort(ctx context.Context, acl ACL, ipport netaddr.IPPort) (res *ACLPreview, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.PreviewACLForIPPort: %w", err)
}
}()
postData, err := json.Marshal(acl.ACL)
if err != nil {
return nil, err
}
b, err := c.previewACLPostRequest(ctx, postData, "ipport", ipport.String())
if err != nil {
return nil, err
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
}, nil
}
// PreviewACLHuJSONForUser determines what rules match a given ACL for a user.
// The ACL can be a locally modified or clean ACL obtained from server.
//
// Returns ACLPreview on success with matches in a slice. If there are no matches,
// the call is still successful but Matches will be an empty slice.
// Returns error if the provided ACL is invalid.
func (c *Client) PreviewACLHuJSONForUser(ctx context.Context, acl ACLHuJSON, user string) (res *ACLPreview, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.PreviewACLHuJSONForUser: %w", err)
}
}()
postData := []byte(acl.ACL)
b, err := c.previewACLPostRequest(ctx, postData, "user", user)
if err != nil {
return nil, err
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
}, nil
}
// PreviewACLHuJSONForIPPort determines what rules match a given ACL for a ipport.
// The ACL can be a locally modified or clean ACL obtained from server.
//
// Returns ACLPreview on success with matches in a slice. If there are no matches,
// the call is still successful but Matches will be an empty slice.
// Returns error if the provided ACL is invalid.
func (c *Client) PreviewACLHuJSONForIPPort(ctx context.Context, acl ACLHuJSON, ipport string) (res *ACLPreview, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.PreviewACLHuJSONForIPPort: %w", err)
}
}()
postData := []byte(acl.ACL)
b, err := c.previewACLPostRequest(ctx, postData, "ipport", ipport)
if err != nil {
return nil, err
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
}, nil
}
// ValidateACLJSON takes in the given source and destination (in this situation,
// it is assumed that you are checking whether the source can connect to destination)
// and creates an ACLTest from that. It then sends the ACLTest to the control api acl
// validate endpoint, where the test is run. It returns a nil ACLTestError pointer if
// no test errors occur.
func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (testErr *ACLTestError, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.ValidateACLJSON: %w", err)
}
}()
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
postData, err := json.Marshal(tests)
if err != nil {
return nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
c.setAuth(req)
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode)
}
// The test ran without fail
if len(b) == 0 {
return nil, nil
}
var res ACLTestError
// The test returned errors.
if err = json.Unmarshal(b, &res); err != nil {
// failed to unmarshal
return nil, err
}
return &res, nil
}

View File

@@ -2,7 +2,7 @@
// 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.
// Package apitype contains types for the Tailscale local API and control plane API.
package apitype
import "tailscale.com/tailcfg"
@@ -11,6 +11,9 @@ import "tailscale.com/tailcfg"
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
// Caps are extra capabilities that the remote Node has to this node.
Caps []string `json:",omitempty"`
}
// FileTarget is a node to which files can be sent, and the PeerAPI

View File

@@ -0,0 +1,20 @@
// 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 apitype
type DNSConfig struct {
Resolvers []DNSResolver `json:"resolvers"`
FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
Routes map[string][]DNSResolver `json:"routes"`
Domains []string `json:"domains"`
Nameservers []string `json:"nameservers"`
Proxied bool `json:"proxied"`
PerDomain bool `json:",omitempty"`
}
type DNSResolver struct {
Addr string `json:"addr"`
BootstrapResolution []string `json:"bootstrapResolution,omitempty"`
}

262
client/tailscale/devices.go Normal file
View File

@@ -0,0 +1,262 @@
// 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.18
// +build go1.18
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"tailscale.com/types/opt"
)
type GetDevicesResponse struct {
Devices []*Device `json:"devices"`
}
type DerpRegion struct {
Preferred bool `json:"preferred,omitempty"`
LatencyMilliseconds float64 `json:"latencyMs"`
}
type ClientConnectivity struct {
Endpoints []string `json:"endpoints"`
DERP string `json:"derp"`
MappingVariesByDestIP opt.Bool `json:"mappingVariesByDestIP"`
// DERPLatency is mapped by region name (e.g. "New York City", "Seattle").
DERPLatency map[string]DerpRegion `json:"latency"`
ClientSupports map[string]opt.Bool `json:"clientSupports"`
}
type Device struct {
// Addresses is a list of the devices's Tailscale IP addresses.
// It's currently just 1 element, the 100.x.y.z Tailscale IP.
Addresses []string `json:"addresses"`
DeviceID string `json:"id"`
User string `json:"user"`
Name string `json:"name"`
Hostname string `json:"hostname"`
ClientVersion string `json:"clientVersion"` // Empty for external devices.
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
OS string `json:"os"`
Created string `json:"created"` // Empty for external devices.
LastSeen string `json:"lastSeen"`
KeyExpiryDisabled bool `json:"keyExpiryDisabled"`
Expires string `json:"expires"`
Authorized bool `json:"authorized"`
IsExternal bool `json:"isExternal"`
MachineKey string `json:"machineKey"` // Empty for external devices.
NodeKey string `json:"nodeKey"`
// BlocksIncomingConnections is configured via the device's
// Tailscale client preferences. This field is only reported
// to the API starting with Tailscale 1.3.x clients.
BlocksIncomingConnections bool `json:"blocksIncomingConnections"`
// The following fields are not included by default:
// EnabledRoutes are the previously-approved subnet routes
// (e.g. "192.168.4.16/24", "10.5.2.4/32").
EnabledRoutes []string `json:"enabledRoutes"` // Empty for external devices.
// AdvertisedRoutes are the subnets (both enabled and not enabled)
// being requested from the node.
AdvertisedRoutes []string `json:"advertisedRoutes"` // Empty for external devices.
ClientConnectivity *ClientConnectivity `json:"clientConnectivity"`
}
// DeviceFieldsOpts determines which fields should be returned in the response.
//
// Please only use DeviceAllFields and DeviceDefaultFields.
// Other DeviceFieldsOpts are not supported.
//
// TODO: Support other DeviceFieldsOpts.
// In the future, users should be able to create their own DeviceFieldsOpts
// as valid arguments by setting the fields they want returned to a "non-nil"
// value. For example, DeviceFieldsOpts{NodeID: "true"} should only return NodeIDs.
type DeviceFieldsOpts Device
func (d *DeviceFieldsOpts) addFieldsToQueryParameter() string {
if d == DeviceDefaultFields || d == nil {
return "default"
}
if d == DeviceAllFields {
return "all"
}
return ""
}
var (
DeviceAllFields = &DeviceFieldsOpts{}
// DeviceDefaultFields specifies that the following fields are returned:
// Addresses, NodeID, User, Name, Hostname, ClientVersion, UpdateAvailable,
// OS, Created, LastSeen, KeyExpiryDisabled, Expires, Authorized, IsExternal
// MachineKey, NodeKey, BlocksIncomingConnections.
DeviceDefaultFields = &DeviceFieldsOpts{}
)
// Devices retrieves the list of devices for a tailnet.
//
// See the Device structure for the list of fields hidden for external devices.
// The optional fields parameter specifies which fields of the devices to return; currently
// only DeviceDefaultFields (equivalent to nil) and DeviceAllFields are supported.
// Other values are currently undefined.
func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceList []*Device, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.Devices: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.baseURL(), c.tailnet)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
// Add fields.
fieldStr := fields.addFieldsToQueryParameter()
q := req.URL.Query()
q.Add("fields", fieldStr)
req.URL.RawQuery = q.Encode()
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
var devices GetDevicesResponse
err = json.Unmarshal(b, &devices)
return devices.Devices, err
}
// Device retrieved the details for a specific device.
//
// See the Device structure for the list of fields hidden for an external device.
// The optional fields parameter specifies which fields of the devices to return; currently
// only DeviceDefaultFields (equivalent to nil) and DeviceAllFields are supported.
// Other values are currently undefined.
func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFieldsOpts) (device *Device, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.Device: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/device/%s", c.baseURL(), deviceID)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
// Add fields.
fieldStr := fields.addFieldsToQueryParameter()
q := req.URL.Query()
q.Add("fields", fieldStr)
req.URL.RawQuery = q.Encode()
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
err = json.Unmarshal(b, &device)
return device, err
}
// DeleteDevice deletes the specified device from the Client's tailnet.
// NOTE: Only devices that belong to the Client's tailnet can be deleted.
// Deleting external devices is not supported.
func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.DeleteDevice: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/device/%s", c.baseURL(), url.PathEscape(deviceID))
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 status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// AuthorizeDevice marks a device as authorized.
func (c *Client) AuthorizeDevice(ctx context.Context, deviceID string) error {
path := fmt.Sprintf("%s/api/v2/device/%s/authorized", c.baseURL(), url.PathEscape(deviceID))
req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(`{"authorized":true}`))
if err != nil {
return err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// SetTags updates the ACL tags on a device.
func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) error {
params := &struct {
Tags []string `json:"tags"`
}{Tags: tags}
data, err := json.Marshal(params)
if err != nil {
return err
}
path := fmt.Sprintf("%s/api/v2/device/%s/tags", c.baseURL(), url.PathEscape(deviceID))
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data))
if err != nil {
return err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}

235
client/tailscale/dns.go Normal file
View File

@@ -0,0 +1,235 @@
// 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.18
// +build go1.18
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"tailscale.com/client/tailscale/apitype"
)
// DNSNameServers is returned when retrieving the list of nameservers.
// It is also the structure provided when setting nameservers.
type DNSNameServers struct {
DNS []string `json:"dns"` // DNS name servers
}
// DNSNameServersPostResponse is returned when setting the list of DNS nameservers.
//
// It includes the MagicDNS status since nameservers changes may affect MagicDNS.
type DNSNameServersPostResponse struct {
DNS []string `json:"dns"` // DNS name servers
MagicDNS bool `json:"magicDNS"` // whether MagicDNS is active for this tailnet (enabled + has fallback nameservers)
}
// DNSSearchpaths is the list of search paths for a given domain.
type DNSSearchPaths struct {
SearchPaths []string `json:"searchPaths"` // DNS search paths
}
// DNSPreferences is the preferences set for a given tailnet.
//
// It includes MagicDNS which can be turned on or off. To enable MagicDNS,
// there must be at least one nameserver. When all nameservers are removed,
// MagicDNS is disabled.
type DNSPreferences struct {
MagicDNS bool `json:"magicDNS"` // whether MagicDNS is active for this tailnet (enabled + has fallback nameservers)
}
func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
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 status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData interface{}) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
data, err := json.Marshal(&postData)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data))
req.Header.Set("Content-Type", "application/json")
if err != nil {
return nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
return b, nil
}
// DNSConfig retrieves the DNSConfig settings for a domain.
func (c *Client) DNSConfig(ctx context.Context) (cfg *apitype.DNSConfig, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.DNSConfig: %w", err)
}
}()
b, err := c.dnsGETRequest(ctx, "config")
if err != nil {
return nil, err
}
var dnsResp apitype.DNSConfig
err = json.Unmarshal(b, &dnsResp)
return &dnsResp, err
}
func (c *Client) SetDNSConfig(ctx context.Context, cfg apitype.DNSConfig) (resp *apitype.DNSConfig, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetDNSConfig: %w", err)
}
}()
var dnsResp apitype.DNSConfig
b, err := c.dnsPOSTRequest(ctx, "config", cfg)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &dnsResp)
return &dnsResp, err
}
// NameServers retrieves the list of nameservers set for a domain.
func (c *Client) NameServers(ctx context.Context) (nameservers []string, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.NameServers: %w", err)
}
}()
b, err := c.dnsGETRequest(ctx, "nameservers")
if err != nil {
return nil, err
}
var dnsResp DNSNameServers
err = json.Unmarshal(b, &dnsResp)
return dnsResp.DNS, err
}
// SetNameServers sets the list of nameservers for a tailnet to the list provided
// by the user.
//
// It returns the new list of nameservers and the MagicDNS status in case it was
// affected by the change. For example, removing all nameservers will turn off
// MagicDNS.
func (c *Client) SetNameServers(ctx context.Context, nameservers []string) (dnsResp *DNSNameServersPostResponse, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetNameServers: %w", err)
}
}()
dnsReq := DNSNameServers{DNS: nameservers}
b, err := c.dnsPOSTRequest(ctx, "nameservers", dnsReq)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &dnsResp)
return dnsResp, err
}
// DNSPreferences retrieves the DNS preferences set for a tailnet.
//
// It returns the status of MagicDNS.
func (c *Client) DNSPreferences(ctx context.Context) (dnsResp *DNSPreferences, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.DNSPreferences: %w", err)
}
}()
b, err := c.dnsGETRequest(ctx, "preferences")
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &dnsResp)
return dnsResp, err
}
// SetDNSPreferences sets the DNS preferences for a tailnet.
//
// MagicDNS can only be enabled when there is at least one nameserver provided.
// When all nameservers are removed, MagicDNS is disabled and will stay disabled,
// unless explicitly enabled by a user again.
func (c *Client) SetDNSPreferences(ctx context.Context, magicDNS bool) (dnsResp *DNSPreferences, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetDNSPreferences: %w", err)
}
}()
dnsReq := DNSPreferences{MagicDNS: magicDNS}
b, err := c.dnsPOSTRequest(ctx, "preferences", dnsReq)
if err != nil {
return
}
err = json.Unmarshal(b, &dnsResp)
return dnsResp, err
}
// SearchPaths retrieves the list of searchpaths set for a tailnet.
func (c *Client) SearchPaths(ctx context.Context) (searchpaths []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SearchPaths: %w", err)
}
}()
b, err := c.dnsGETRequest(ctx, "searchpaths")
if err != nil {
return nil, err
}
var dnsResp *DNSSearchPaths
err = json.Unmarshal(b, &dnsResp)
return dnsResp.SearchPaths, err
}
// SetSearchPaths sets the list of searchpaths for a tailnet.
func (c *Client) SetSearchPaths(ctx context.Context, searchpaths []string) (newSearchPaths []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetSearchPaths: %w", err)
}
}()
dnsReq := DNSSearchPaths{SearchPaths: searchpaths}
b, err := c.dnsPOSTRequest(ctx, "searchpaths", dnsReq)
if err != nil {
return nil, err
}
var dnsResp DNSSearchPaths
err = json.Unmarshal(b, &dnsResp)
return dnsResp.SearchPaths, err
}

View File

@@ -0,0 +1,711 @@
// Copyright (c) 2021 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.18
// +build go1.18
package tailscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
)
// 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
// 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
// for details.
//
// Its zero value is valid to use.
//
// Any exported fields should be set before using methods on the type
// and not changed thereafter.
type LocalClient struct {
// Dial optionally specifies an alternate func that connects to the local
// machine's tailscaled or equivalent. If nil, a default is used.
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
// Socket specifies an alternate path to the local Tailscale socket.
// If empty, a platform-specific default is used.
Socket string
// UseSocketOnly, if true, tries to only connect to tailscaled via the
// Unix socket and not via fallback mechanisms as done on macOS when
// connecting to the GUI client variants.
UseSocketOnly bool
// tsClient does HTTP requests to the local Tailscale daemon.
// It's lazily initialized on first use.
tsClient *http.Client
tsClientOnce sync.Once
}
func (lc *LocalClient) socket() string {
if lc.Socket != "" {
return lc.Socket
}
return paths.DefaultTailscaledSocket()
}
func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
if lc.Dial != nil {
return lc.Dial
}
return lc.defaultDialer
}
func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
if !lc.UseSocketOnly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
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)
}
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
//
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
//
// The hostname must be "local-tailscaled.sock", even though it
// doesn't actually do any DNS lookup. The actual means of connecting to and
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
lc.tsClientOnce.Do(func() {
lc.tsClient = &http.Client{
Transport: &http.Transport{
DialContext: lc.dialer(),
},
}
})
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
return lc.tsClient.Do(req)
}
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 res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
path := req.URL.Path
pathPrefix, _, _ := strings.Cut(path, "?")
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
}
type errorJSON struct {
Error string
}
// AccessDeniedError is an error due to permissions.
type AccessDeniedError struct {
err error
}
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
func (e *AccessDeniedError) Unwrap() error { return e.err }
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
func IsAccessDeniedError(err error) bool {
var ae *AccessDeniedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return errors.New(j.Error)
}
return err
}
func errorMessageFromBody(body []byte) string {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return j.Error
}
return strings.TrimSpace(string(body))
}
var onVersionMismatch func(clientVer, serverVer string)
// SetVersionMismatchHandler sets f as the version mismatch handler
// to be called when the client (the current process) has a version
// number that doesn't match the server's declared version.
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
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 err != nil {
return nil, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != wantStatus {
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
return nil, bestError(err, slurp)
}
return slurp, nil
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
return lc.send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
//
// Deprecated: use LocalClient.WhoIs.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
return defaultLocalClient.WhoIs(ctx, remoteAddr)
}
// 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
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func (lc *LocalClient) Goroutines(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
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) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
}
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))
}
// 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)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// 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 {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
}
// Status returns the Tailscale daemon's status.
func (lc *LocalClient) Status(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.StatusWithoutPeers(ctx)
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func (lc *LocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "?peers=false")
}
func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := lc.get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
st := new(ipnstate.Status)
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
}
// IDToken is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
if err != nil {
return nil, err
}
tr := new(tailcfg.TokenResponse)
if err := json.Unmarshal(body, tr); err != nil {
return nil, err
}
return tr, nil
}
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := lc.get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
}
func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
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)
if err != nil {
return nil, 0, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, 0, err
}
if res.ContentLength == -1 {
res.Body.Close()
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, res.ContentLength, nil
}
func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := lc.get200(ctx, "/localapi/v0/file-targets")
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
}
// PushFile sends Taildrop file r to target.
//
// 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)
if err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
}
// CheckIPForwarding asks the local Tailscale daemon whether it looks like the
// machine is properly configured to forward IP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
// CheckPrefs validates the provided preferences, without making any changes.
//
// The CLI uses this before a Start call to fail fast if the preferences won't
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
// 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))
return err
}
func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := lc.get200(ctx, "/localapi/v0/prefs")
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
}
func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(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
}
func (lc *LocalClient) Logout(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// DialTCP connects to the host's port via Tailscale.
//
// The host may be a base DNS name (resolved from the netmap inside
// tailscaled), a FQDN, or an IP address.
//
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
connCh <- info.Conn
},
}
ctx = httptrace.WithClientTrace(ctx, &trace)
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
if err != nil {
return nil, err
}
req.Header = http.Header{
"Upgrade": []string{"ts-dial"},
"Connection": []string{"upgrade"},
"Dial-Host": []string{host},
"Dial-Port": []string{fmt.Sprint(port)},
}
res, err := lc.DoLocalRequest(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusSwitchingProtocols {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("unexpected HTTP response: %s, %s", res.Status, body)
}
// From here on, the underlying net.Conn is ours to use, but there
// is still a read buffer attached to it within resp.Body. So, we
// must direct I/O through resp.Body, but we can still use the
// underlying net.Conn for stuff like deadlines.
var switchedConn net.Conn
select {
case switchedConn = <-connCh:
default:
}
if switchedConn == nil {
res.Body.Close()
return nil, fmt.Errorf("httptrace didn't provide a connection")
}
rwc, ok := res.Body.(io.ReadWriteCloser)
if !ok {
res.Body.Close()
return nil, errors.New("http Transport did not provide a writable body")
}
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use LocalClient.CertPair.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return defaultLocalClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// Deprecated: use LocalClient.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultLocalClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := lc.ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := lc.CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the 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.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}
// Ping sends a ping of the provided type to the provided IP and waits
// for its response.
func (lc *LocalClient) Ping(ctx context.Context, ip netaddr.IP, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
v := url.Values{}
v.Set("ip", ip.String())
v.Set("type", string(pingtype))
body, err := lc.send(ctx, "POST", "/localapi/v0/ping?"+v.Encode(), 200, nil)
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
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//
// It ends in a punctuation. See caller.
func tailscaledConnectHint() string {
if runtime.GOOS != "linux" {
// TODO(bradfitz): flesh this out
return "not running?"
}
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
if err != nil {
return "not running?"
}
// Parse:
// LoadState=loaded
// ActiveState=inactive
// SubState=dead
st := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
st[k] = strings.TrimSpace(v)
}
}
if st["LoadState"] == "loaded" &&
(st["SubState"] != "running" || st["ActiveState"] != "active") {
return "systemd tailscaled.service not running."
}
return "not running?"
}

View File

@@ -0,0 +1,98 @@
// 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.18
// +build go1.18
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"inet.af/netaddr"
)
// Routes contains the lists of subnet routes that are currently advertised by a device,
// as well as the subnets that are enabled to be routed by the device.
type Routes struct {
AdvertisedRoutes []netaddr.IPPrefix `json:"advertisedRoutes"`
EnabledRoutes []netaddr.IPPrefix `json:"enabledRoutes"`
}
// Routes retrieves the list of subnet routes that have been enabled for a device.
// The routes that are returned are not necessarily advertised by the device,
// they have only been preapproved.
func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.Routes: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.baseURL(), deviceID)
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 status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
var sr Routes
err = json.Unmarshal(b, &sr)
return &sr, err
}
type postRoutesParams struct {
Routes []netaddr.IPPrefix `json:"routes"`
}
// SetRoutes updates the list of subnets that are enabled for a device.
// Subnets must be parsable by inet.af/netaddr.ParseIPPrefix.
// Subnets do not have to be currently advertised by a device, they may be pre-enabled.
// Returns the updated list of enabled and advertised subnet routes in a *Routes object.
func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netaddr.IPPrefix) (routes *Routes, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetRoutes: %w", err)
}
}()
params := &postRoutesParams{Routes: subnets}
data, err := json.Marshal(params)
if err != nil {
return nil, err
}
path := fmt.Sprintf("%s/api/v2/device/%s/routes", c.baseURL(), deviceID)
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, err
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
var srr *Routes
if err := json.Unmarshal(b, &srr); err != nil {
return nil, err
}
return srr, err
}

View File

@@ -0,0 +1,42 @@
// 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.18
// +build go1.18
package tailscale
import (
"context"
"fmt"
"net/http"
"net/url"
)
// TailnetDeleteRequest handles sending a DELETE request for a tailnet to control.
func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.DeleteTailnet: %w", err)
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.baseURL(), url.PathEscape(string(tailnetID)))
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil)
if err != nil {
return err
}
c.setAuth(req)
b, resp, err := c.sendRequest(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}

View File

@@ -1,529 +1,160 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// 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.18
// +build go1.18
// Package tailscale contains Tailscale client code.
// Package tailscale contains Go clients for the Tailscale Local API and
// Tailscale control plane API.
//
// Warning: this package is in development and makes no API compatibility
// promises as of 2022-04-29. It is subject to change at any time.
package tailscale
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"time"
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/version"
)
var (
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
TailscaledSocket = paths.DefaultTailscaledSocket()
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
TailscaledSocketSetExplicitly bool
// TailscaledDialer is the DialContext func that connects to the local machine's
// tailscaled or equivalent.
TailscaledDialer = defaultDialer
)
func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
// TODO: make this part of a safesocket.ConnectionStrategy
if !TailscaledSocketSetExplicitly {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
// The user provided a non-default tailscaled socket address.
// Connect only to exactly what they provided.
s.UseFallback(false)
return safesocket.Connect(s)
}
var (
// tsClient does HTTP requests to the local Tailscale daemon.
// We lazily initialize the client in case the caller wants to
// override TailscaledDialer.
tsClient *http.Client
tsClientOnce sync.Once
)
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
// for now. It was added 2022-04-29 when it was moved to this git repo
// and will be removed when the public API has settled.
//
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
// TODO(bradfitz): remove this after the we're happy with the public API.
var I_Acknowledge_This_API_Is_Unstable = false
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
const defaultAPIBase = "https://api.tailscale.com"
// maxSize is the maximum read size (10MB) of responses from the server.
const maxReadSize = 10 << 20
// Client makes API calls to the Tailscale control plane API server.
//
// The hostname must be "local-tailscaled.sock", even though it
// doesn't actually do any DNS lookup. The actual means of connecting to and
// authenticating to the local Tailscale daemon vary by platform.
// Use NewClient to instantiate one. Exported fields should be set before
// the client is used and not changed thereafter.
type Client struct {
// tailnet is the globally unique identifier for a Tailscale network, such
// as "example.com" or "user@gmail.com".
tailnet string
// auth is the authentication method to use for this client.
// nil means none, which generally won't work, but won't crash.
auth AuthMethod
// BaseURL optionally specifies an alternate API server to use.
// If empty, "https://api.tailscale.com" is used.
BaseURL string
// HTTPClient optionally specifies an alternate HTTP client to use.
// If nil, http.DefaultClient is used.
HTTPClient *http.Client
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return http.DefaultClient
}
func (c *Client) baseURL() string {
if c.BaseURL != "" {
return c.BaseURL
}
return defaultAPIBase
}
// AuthMethod is the interface for API authentication methods.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func DoLocalRequest(req *http.Request) (*http.Response, error) {
tsClientOnce.Do(func() {
tsClient = &http.Client{
Transport: &http.Transport{
DialContext: TailscaledDialer,
},
}
})
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
return tsClient.Do(req)
// Most users will use AuthKey.
type AuthMethod interface {
modifyRequest(req *http.Request)
}
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
onVersionMismatch(version.Long, server)
}
if res.StatusCode == 403 {
all, _ := ioutil.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
path := req.URL.Path
pathPrefix, _, _ := strings.Cut(path, "?")
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
}
}
return nil, err
// APIKey is an AuthMethod for NewClient that authenticates requests
// using an authkey.
type APIKey string
func (ak APIKey) modifyRequest(req *http.Request) {
req.SetBasicAuth(string(ak), "")
}
type errorJSON struct {
Error string
func (c *Client) setAuth(r *http.Request) {
if c.auth != nil {
c.auth.modifyRequest(r)
}
}
// AccessDeniedError is an error due to permissions.
type AccessDeniedError struct {
err error
}
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
func (e *AccessDeniedError) Unwrap() error { return e.err }
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
func IsAccessDeniedError(err error) bool {
var ae *AccessDeniedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return errors.New(j.Error)
}
return err
}
func errorMessageFromBody(body []byte) string {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return j.Error
}
return strings.TrimSpace(string(body))
}
var onVersionMismatch func(clientVer, serverVer string)
// SetVersionMismatchHandler sets f as the version mismatch handler
// to be called when the client (the current process) has a version
// number that doesn't match the server's declared version.
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
func 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 err != nil {
return nil, err
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != wantStatus {
return nil, bestError(err, slurp)
}
return slurp, nil
}
func get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := 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
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func DaemonMetrics(ctx context.Context) ([]byte, error) {
return get200(ctx, "/localapi/v0/metrics")
}
// Profile returns a pprof profile of the Tailscale daemon.
func Profile(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")
}
if sec != 0 || pprofType == "profile" {
secArg = fmt.Sprint(sec)
}
return get200(ctx, fmt.Sprintf("/localapi/v0/profile?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 BugReport(ctx context.Context, note string) (string, error) {
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func DebugAction(ctx context.Context, action string) error {
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "?peers=false")
}
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
st := new(ipnstate.Status)
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
}
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
}
func DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func 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)
if err != nil {
return nil, 0, err
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return nil, 0, err
}
if res.ContentLength == -1 {
res.Body.Close()
return nil, 0, fmt.Errorf("unexpected chunking")
}
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, res.ContentLength, nil
}
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets")
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
}
// PushFile sends Taildrop file r to target.
// NewClient is a convenience method for instantiating a new Client.
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func 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)
// tailnet is the globally unique identifier for a Tailscale network, such
// as "example.com" or "user@gmail.com".
// If httpClient is nil, then http.DefaultClient is used.
// "api.tailscale.com" is set as the BaseURL for the returned client
// and can be changed manually by the user.
func NewClient(tailnet string, auth AuthMethod) *Client {
return &Client{
tailnet: tailnet,
auth: auth,
}
}
func (c *Client) Tailnet() string { return c.tailnet }
// Do sends a raw HTTP request, after adding any authentication headers.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
if !I_Acknowledge_This_API_Is_Unstable {
return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
}
c.setAuth(req)
return c.httpClient().Do(req)
}
// sendRequest add the authenication 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 {
return nil, nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
}
c.setAuth(req)
resp, err := c.httpClient().Do(req)
if err != nil {
return nil, resp, err
}
defer resp.Body.Close()
// Read response. Limit the response to 10MB.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := ioutil.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}
return b, resp, err
}
// ErrResponse is the HTTP error returned by the Tailscale server.
type ErrResponse struct {
Status int
Message string
}
func (e ErrResponse) Error() string {
return fmt.Sprintf("Status: %d, Message: %q", e.Status, e.Message)
}
// handleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
func handleErrorResponse(b []byte, resp *http.Response) error {
var errResp ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return err
}
if size != -1 {
req.ContentLength = size
}
res, err := doLocalRequestNiceError(req)
if err != nil {
return err
}
if res.StatusCode == 200 {
io.Copy(io.Discard, res.Body)
return nil
}
all, _ := io.ReadAll(res.Body)
return bestError(fmt.Errorf("%s: %s", res.Status, all), all)
}
func CheckIPForwarding(ctx context.Context) error {
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
var jres struct {
Warning string
}
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
return errors.New(jres.Warning)
}
return nil
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
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
}
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
if err != nil {
return nil, err
}
body, err := 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
}
func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
return nil, err
}
if err = json.Unmarshal(res, &derpMap); err != nil {
return nil, fmt.Errorf("invalid derp map json: %w", err)
}
return &derpMap, nil
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//
// It ends in a punctuation. See caller.
func tailscaledConnectHint() string {
if runtime.GOOS != "linux" {
// TODO(bradfitz): flesh this out
return "not running?"
}
out, err := exec.Command("systemctl", "show", "tailscaled.service", "--no-page", "--property", "LoadState,ActiveState,SubState").Output()
if err != nil {
return "not running?"
}
// Parse:
// LoadState=loaded
// ActiveState=inactive
// SubState=dead
st := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
st[k] = strings.TrimSpace(v)
}
}
if st["LoadState"] == "loaded" &&
(st["SubState"] != "running" || st["ActiveState"] != "active") {
return "systemd tailscaled.service not running."
}
return "not running?"
errResp.Status = resp.StatusCode
return errResp
}

View File

@@ -22,13 +22,11 @@ import (
"os"
"strings"
"golang.org/x/tools/go/packages"
"tailscale.com/util/codegen"
)
var (
flagTypes = flag.String("type", "", "comma-separated list of types; required")
flagOutput = flag.String("output", "", "output file; required")
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
)
@@ -43,30 +41,18 @@ func main() {
}
typeNames := strings.Split(*flagTypes, ",")
cfg := &packages.Config{
Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedName,
Tests: false,
}
if *flagBuildTags != "" {
cfg.BuildFlags = []string{"-tags=" + *flagBuildTags}
}
pkgs, err := packages.Load(cfg, ".")
pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
if err != nil {
log.Fatal(err)
}
if len(pkgs) != 1 {
log.Fatalf("wrong number of packages: %d", len(pkgs))
}
pkg := pkgs[0]
it := codegen.NewImportTracker(pkg.Types)
buf := new(bytes.Buffer)
imports := make(map[string]struct{})
namedTypes := codegen.NamedTypes(pkg)
for _, typeName := range typeNames {
typ, ok := namedTypes[typeName]
if !ok {
log.Fatalf("could not find type %s", typeName)
}
gen(buf, imports, typ, pkg.Types)
gen(buf, it, typ)
}
w := func(format string, args ...any) {
@@ -93,62 +79,13 @@ func main() {
w(" return false")
w("}")
}
contents := new(bytes.Buffer)
var flagArgs []string
if *flagTypes != "" {
flagArgs = append(flagArgs, "-type="+*flagTypes)
}
if *flagOutput != "" {
flagArgs = append(flagArgs, "-output="+*flagOutput)
}
if *flagBuildTags != "" {
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
}
if *flagCloneFunc {
flagArgs = append(flagArgs, "-clonefunc")
}
fmt.Fprintf(contents, header, strings.Join(flagArgs, " "), pkg.Name)
fmt.Fprintf(contents, "import (\n")
for s := range imports {
fmt.Fprintf(contents, "\t%q\n", s)
}
fmt.Fprintf(contents, ")\n\n")
contents.Write(buf.Bytes())
output := *flagOutput
if output == "" {
flag.Usage()
os.Exit(2)
}
if err := codegen.WriteFormatted(contents.Bytes(), output); err != nil {
cloneOutput := pkg.Name + "_clone.go"
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
log.Fatal(err)
}
}
const header = `// 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.
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
//` + `go:generate` + ` go run tailscale.com/cmd/cloner %s
package %s
`
func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisPkg *types.Package) {
pkgQual := func(pkg *types.Package) string {
if thisPkg == pkg {
return ""
}
imports[pkg.Path()] = struct{}{}
return pkg.Name()
}
importedName := func(t types.Type) string {
return types.TypeString(t, pkgQual)
}
func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
t, ok := typ.Underlying().(*types.Struct)
if !ok {
return
@@ -169,11 +106,11 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
for i := 0; i < t.NumFields(); i++ {
fname := t.Field(i).Name()
ft := t.Field(i).Type()
if !codegen.ContainsPointers(ft) {
if !codegen.ContainsPointers(ft) || codegen.HasNoClone(t.Tag(i)) {
continue
}
if named, _ := ft.(*types.Named); named != nil {
if isViewType(ft) {
if codegen.IsViewType(ft) {
writef("dst.%s = src.%s", fname, fname)
continue
}
@@ -185,11 +122,16 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
switch ft := ft.Underlying().(type) {
case *types.Slice:
if codegen.ContainsPointers(ft.Elem()) {
n := importedName(ft.Elem())
n := it.QualifiedName(ft.Elem())
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic {
writef("\tx := *src.%s[i]", fname)
writef("\tdst.%s[i] = &x", fname)
} else {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
}
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
@@ -202,7 +144,7 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
writef("dst.%s = src.%s.Clone()", fname, fname)
continue
}
n := importedName(ft.Elem())
n := it.QualifiedName(ft.Elem())
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = new(%s)", fname, n)
writef("\t*dst.%s = *src.%s", fname, fname)
@@ -212,9 +154,9 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
writef("}")
case *types.Map:
writef("if dst.%s != nil {", fname)
writef("\tdst.%s = map[%s]%s{}", fname, importedName(ft.Key()), importedName(ft.Elem()))
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(ft.Elem()))
if sliceType, isSlice := ft.Elem().(*types.Slice); isSlice {
n := importedName(sliceType.Elem())
n := it.QualifiedName(sliceType.Elem())
writef("\tfor k := range src.%s {", fname)
// use zero-length slice instead of nil to ensure
// the key is always copied.
@@ -237,20 +179,10 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, typ *types.Named, thisP
writef("return dst")
fmt.Fprintf(buf, "}\n\n")
buf.Write(codegen.AssertStructUnchanged(t, thisPkg, name, "Clone", imports))
}
func isViewType(typ types.Type) bool {
t, ok := typ.Underlying().(*types.Struct)
if !ok {
return false
}
if t.NumFields() != 1 {
return false
}
return t.Field(0).Name() == "ж"
buf.Write(codegen.AssertStructUnchanged(t, name, "Clone", it))
}
// hasBasicUnderlying reports true when typ.Underlying() is a slice or a map.
func hasBasicUnderlying(typ types.Type) bool {
switch typ.Underlying().(type) {
case *types.Slice, *types.Map:

View File

@@ -13,7 +13,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/derp/wsconn"
"tailscale.com/net/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")

View File

@@ -6,6 +6,7 @@
package main
import (
"flag"
"fmt"
"log"
"os"
@@ -14,7 +15,6 @@ import (
"github.com/goreleaser/nfpm"
_ "github.com/goreleaser/nfpm/deb"
_ "github.com/goreleaser/nfpm/rpm"
"github.com/pborman/getopt"
)
// parseFiles parses a comma-separated list of colon-separated pairs
@@ -44,19 +44,21 @@ func parseEmptyDirs(s string) []string {
}
func main() {
out := getopt.StringLong("out", 'o', "", "output file to write")
goarch := getopt.StringLong("arch", 'a', "amd64", "GOARCH this package is for")
pkgType := getopt.StringLong("type", 't', "deb", "type of package to build (deb or rpm)")
files := getopt.StringLong("files", 'F', "", "comma-separated list of files in src:dst form")
configFiles := getopt.StringLong("configs", 'C', "", "like --files, but for files marked as user-editable config files")
emptyDirs := getopt.StringLong("emptydirs", 'E', "", "comma-separated list of empty directories")
version := getopt.StringLong("version", 0, "0.0.0", "version of the package")
postinst := getopt.StringLong("postinst", 0, "", "debian postinst script path")
prerm := getopt.StringLong("prerm", 0, "", "debian prerm script path")
postrm := getopt.StringLong("postrm", 0, "", "debian postrm script path")
replaces := getopt.StringLong("replaces", 0, "", "package which this package replaces, if any")
depends := getopt.StringLong("depends", 0, "", "comma-separated list of packages this package depends on")
getopt.Parse()
out := flag.String("out", "", "output file to write")
name := flag.String("name", "tailscale", "package name")
description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
goarch := flag.String("arch", "amd64", "GOARCH this package is for")
pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
files := flag.String("files", "", "comma-separated list of files in src:dst form")
configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
version := flag.String("version", "0.0.0", "version of the package")
postinst := flag.String("postinst", "", "debian postinst script path")
prerm := flag.String("prerm", "", "debian prerm script path")
postrm := flag.String("postrm", "", "debian postrm script path")
replaces := flag.String("replaces", "", "package which this package replaces, if any")
depends := flag.String("depends", "", "comma-separated list of packages this package depends on")
flag.Parse()
filesMap, err := parseFiles(*files)
if err != nil {
@@ -68,12 +70,12 @@ func main() {
}
emptyDirList := parseEmptyDirs(*emptyDirs)
info := nfpm.WithDefaults(&nfpm.Info{
Name: "tailscale",
Name: *name,
Arch: *goarch,
Platform: "linux",
Version: *version,
Maintainer: "Tailscale Inc <info@tailscale.com>",
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO",
Description: *description,
Homepage: "https://www.tailscale.com",
License: "MIT",
Overridables: nfpm.Overridables{

4
cmd/nginx-auth/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
nga.sock
*.deb
*.rpm
tailscale.nginx-auth

157
cmd/nginx-auth/README.md Normal file
View File

@@ -0,0 +1,157 @@
# nginx-auth
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
Tailscale IP of the NGINX server and then seamlessly use Tailscale for
authentication.
Many thanks to [@zrail](https://twitter.com/zrail/status/1511788463586222087) on
Twitter for introducing the basic idea and offering some sample code. This
program is based on that sample code with security enhancements. Namely:
* This listens over a UNIX socket instead of a TCP socket, to prevent
leakage to the network
* This uses systemd socket activation so that systemd owns the socket
and can then lock down the service to the bare minimum required to do
its job without having to worry about dropping permissions
* This provides additional information in HTTP response headers that can
be useful for integrating with various services
## Configuration
In order to protect a service with this tool, do the following in the respective
`server` block:
Create an authentication location with the `internal` flag set:
```nginx
location /auth {
internal;
proxy_pass http://unix:/run/tailscale.nginx-auth.sock;
proxy_pass_request_body off;
proxy_set_header Host $http_host;
proxy_set_header Remote-Addr $remote_addr;
proxy_set_header Remote-Port $remote_port;
proxy_set_header Original-URI $request_uri;
}
```
Then add the following to the `location /` block:
```
auth_request /auth;
auth_request_set $auth_user $upstream_http_tailscale_user;
auth_request_set $auth_name $upstream_http_tailscale_name;
auth_request_set $auth_login $upstream_http_tailscale_login;
auth_request_set $auth_tailnet $upstream_http_tailscale_tailnet;
auth_request_set $auth_profile_picture $upstream_http_tailscale_profile_picture;
proxy_set_header X-Webauth-User "$auth_user";
proxy_set_header X-Webauth-Name "$auth_name";
proxy_set_header X-Webauth-Login "$auth_login";
proxy_set_header X-Webauth-Tailnet "$auth_tailnet";
proxy_set_header X-Webauth-Profile-Picture "$auth_profile_picture";
```
When this configuration is used with a Go HTTP handler such as this:
```go
http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
e := json.NewEncoder(w)
e.SetIndent("", " ")
e.Encode(r.Header)
})
```
You will get output like this:
```json
{
"Accept": [
"*/*"
],
"Connection": [
"upgrade"
],
"User-Agent": [
"curl/7.82.0"
],
"X-Webauth-Login": [
"Xe"
],
"X-Webauth-Name": [
"Xe Iaso"
],
"X-Webauth-Profile-Picture": [
"https://avatars.githubusercontent.com/u/529003?v=4"
],
"X-Webauth-Tailnet": [
"cetacean.org.github"
]
"X-Webauth-User": [
"Xe@github"
]
}
```
## Headers
The authentication service provides the following headers to decorate your
proxied requests:
| Header | Example Value | Description |
| :------ | :-------------- | :---------- |
| `Tailscale-User` | `azurediamond@hunter2.net` | The Tailscale username the remote machine is logged in as in user@host form |
| `Tailscale-Login` | `azurediamond` | The user portion of the Tailscale username the remote machine is logged in as |
| `Tailscale-Name` | `Azure Diamond` | The "real name" of the Tailscale user the machine is logged in as |
| `Tailscale-Profile-Picture` | `https://i.kym-cdn.com/photos/images/newsfeed/001/065/963/ae0.png` | The profile picture provided by the Identity Provider your tailnet uses |
| `Tailscale-Tailnet` | `hunter2.net` | The tailnet name |
Most of the time you can set `X-Webauth-User` to the contents of the
`Tailscale-User` header, but some services may not accept a username with an `@`
symbol in it. If this is the case, set `X-Webauth-User` to the `Tailscale-Login`
header.
The `Tailscale-Tailnet` header can help you identify which tailnet the session
is coming from. If you are using node sharing, this can help you make sure that
you aren't giving administrative access to people outside your tailnet.
### Allow Requests From Only One Tailnet
If you want to prevent node sharing from allowing users to access a service, add
the `Expected-Tailnet` header to your auth request:
```nginx
location /auth {
# ...
proxy_set_header Expected-Tailnet "tailscale.com";
}
```
If a user from a different tailnet tries to use that service, this will return a
generic "forbidden" error page:
```html
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
```
## Building
Install `cmd/mkpkg`:
```
cd .. && go install ./mkpkg
```
Then run `./mkdeb.sh`. It will emit a `.deb` and `.rpm` package for amd64
machines (Linux uname flag: `x86_64`). You can add these to your deployment
methods as you see fit.

14
cmd/nginx-auth/deb/postinst.sh Executable file
View File

@@ -0,0 +1,14 @@
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
else
deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
fi
if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
systemctl --system daemon-reload >/dev/null || true
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
fi
fi

19
cmd/nginx-auth/deb/postrm.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
set -e
if [ -d /run/systemd/system ] ; then
systemctl --system daemon-reload >/dev/null || true
fi
if [ -x "/usr/bin/deb-systemd-helper" ]; then
if [ "$1" = "remove" ]; then
deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
fi
if [ "$1" = "purge" ]; then
deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
fi
fi

8
cmd/nginx-auth/deb/prerm.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -d /run/systemd/system ]; then
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
fi
fi

31
cmd/nginx-auth/mkdeb.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.1
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--arch=amd64 \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=amd64 \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md

View File

@@ -0,0 +1,127 @@
// 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
// Command nginx-auth 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 Tailscale IP of the NGINX server and
// then seamlessly use Tailscale for authentication.
package main
import (
"flag"
"log"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
"github.com/coreos/go-systemd/activation"
"tailscale.com/client/tailscale"
)
var (
sockPath = flag.String("sockpath", "", "the filesystem path for the unix socket this service exposes")
)
func main() {
flag.Parse()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
remoteHost := r.Header.Get("Remote-Addr")
remotePort := r.Header.Get("Remote-Port")
if remoteHost == "" || remotePort == "" {
w.WriteHeader(http.StatusBadRequest)
log.Println("set Remote-Addr to $remote_addr and Remote-Port to $remote_port in your nginx config")
return
}
remoteAddrStr := net.JoinHostPort(remoteHost, remotePort)
remoteAddr, err := netip.ParseAddrPort(remoteAddrStr)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
log.Printf("remote address and port are not valid: %v", err)
return
}
info, err := tailscale.WhoIs(r.Context(), remoteAddr.String())
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
log.Printf("can't look up %s: %v", remoteAddr, err)
return
}
if len(info.Node.Tags) != 0 {
w.WriteHeader(http.StatusForbidden)
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
return
}
_, tailnet, ok := strings.Cut(info.Node.Name, info.Node.ComputedName+".")
if !ok {
w.WriteHeader(http.StatusUnauthorized)
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
}
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
w.WriteHeader(http.StatusForbidden)
log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
return
}
h := w.Header()
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
h.Set("Tailscale-User", info.UserProfile.LoginName)
h.Set("Tailscale-Name", info.UserProfile.DisplayName)
h.Set("Tailscale-Profile-Picture", info.UserProfile.ProfilePicURL)
h.Set("Tailscale-Tailnet", tailnet)
w.WriteHeader(http.StatusNoContent)
})
if *sockPath != "" {
_ = os.Remove(*sockPath) // ignore error, this file may not already exist
ln, err := net.Listen("unix", *sockPath)
if err != nil {
log.Fatalf("can't listen on %s: %v", *sockPath, err)
}
defer ln.Close()
log.Printf("listening on %s", *sockPath)
log.Fatal(http.Serve(ln, mux))
}
listeners, err := activation.Listeners()
if err != nil {
log.Fatalf("no sockets passed to this service with systemd: %v", err)
}
// NOTE(Xe): normally you'd want to make a waitgroup here and then register
// each listener with it. In this case I want this to blow up horribly if
// any of the listeners stop working. systemd will restart it due to the
// socket activation at play.
//
// TL;DR: Let it crash, it will come back
for _, ln := range listeners {
go func(ln net.Listener) {
log.Printf("listening on %s", ln.Addr())
log.Fatal(http.Serve(ln, mux))
}(ln)
}
for {
select {}
}
}

0
cmd/nginx-auth/rpm/postinst.sh Executable file
View File

9
cmd/nginx-auth/rpm/postrm.sh Executable file
View File

@@ -0,0 +1,9 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
systemctl daemon-reload >/dev/null 2>&1 || :
if [ $1 -ge 1 ] ; then
# Package upgrade, not uninstall
systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
fi

9
cmd/nginx-auth/rpm/prerm.sh Executable file
View File

@@ -0,0 +1,9 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
fi

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Tailscale NGINX Authentication service
After=nginx.service
Wants=nginx.service
[Service]
ExecStart=/usr/sbin/tailscale.nginx-auth
DynamicUser=yes
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Tailscale NGINX Authentication socket
PartOf=tailscale.nginx-auth.service
[Socket]
ListenStream=/var/run/tailscale.nginx-auth.sock
[Install]
WantedBy=sockets.target

View File

@@ -0,0 +1,159 @@
// 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.
// proxy-to-grafana is a reverse proxy which identifies users based on their
// originating Tailscale identity and maps them to corresponding Grafana
// users, creating them if needed.
//
// It uses Grafana's AuthProxy feature:
// https://grafana.com/docs/grafana/latest/auth/auth-proxy/
//
// Set the TS_AUTHKEY environment variable to have this server automatically
// join your tailnet, or look for the logged auth link on first start.
//
// Use this Grafana configuration to enable the auth proxy:
//
// [auth.proxy]
// enabled = true
// header_name = X-WEBAUTH-USER
// header_property = username
// auto_sign_up = true
// whitelist = 127.0.0.1
// headers = Name:X-WEBAUTH-NAME
// enable_login_token = true
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
)
var (
hostname = flag.String("hostname", "", "Tailscale hostname to serve on, used as the base name for MagicDNS or subdomain in your domain alias for HTTPS.")
backendAddr = flag.String("backend-addr", "", "Address of the Grafana server served over HTTP, in host:port format. Typically localhost:nnnn.")
tailscaleDir = flag.String("state-dir", "./", "Alternate directory to use for Tailscale state storage. If empty, a default is used.")
useHTTPS = flag.Bool("use-https", false, "Serve over HTTPS via your *.ts.net subdomain if enabled in Tailscale admin.")
)
func main() {
flag.Parse()
if *hostname == "" || strings.Contains(*hostname, ".") {
log.Fatal("missing or invalid --hostname")
}
if *backendAddr == "" {
log.Fatal("missing --backend-addr")
}
ts := &tsnet.Server{
Dir: *tailscaleDir,
Hostname: *hostname,
}
// TODO(bradfitz,maisem): move this to a method on tsnet.Server probably.
if err := ts.Start(); err != nil {
log.Fatalf("Error starting tsnet.Server: %v", err)
}
localClient, _ := ts.LocalClient()
url, err := url.Parse(fmt.Sprintf("http://%s", *backendAddr))
if err != nil {
log.Fatalf("couldn't parse backend address: %v", err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
modifyRequest(req, localClient)
}
var ln net.Listener
if *useHTTPS {
ln, err = ts.Listen("tcp", ":443")
ln = tls.NewListener(ln, &tls.Config{
GetCertificate: tailscale.GetCertificate,
})
go func() {
// wait for tailscale to start before trying to fetch cert names
for i := 0; i < 60; i++ {
st, err := localClient.Status(context.Background())
if err != nil {
log.Printf("error retrieving tailscale status; retrying: %v", err)
} else {
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
}
}
time.Sleep(time.Second)
}
l80, err := ts.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
name, ok := localClient.ExpandSNIName(context.Background(), *hostname)
if !ok {
log.Fatalf("can't get hostname for https redirect")
}
if err := http.Serve(l80, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("https://%s", name), http.StatusMovedPermanently)
})); err != nil {
log.Fatal(err)
}
}()
} else {
ln, err = ts.Listen("tcp", ":80")
}
if err != nil {
log.Fatal(err)
}
log.Printf("proxy-to-grafana running at %v, proxying to %v", ln.Addr(), *backendAddr)
log.Fatal(http.Serve(ln, proxy))
}
func modifyRequest(req *http.Request, localClient *tailscale.LocalClient) {
// with enable_login_token set to true, we get a cookie that handles
// auth for paths that are not /login
if req.URL.Path != "/login" {
return
}
user, err := getTailscaleUser(req.Context(), localClient, req.RemoteAddr)
if err != nil {
log.Printf("error getting Tailscale user: %v", err)
return
}
req.Header.Set("X-Webauth-User", user.LoginName)
req.Header.Set("X-Webauth-Name", user.DisplayName)
}
func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, ipPort string) (*tailcfg.UserProfile, error) {
whois, err := localClient.WhoIs(ctx, ipPort)
if err != nil {
return nil, fmt.Errorf("failed to identify remote host: %w", err)
}
if len(whois.Node.Tags) != 0 {
return nil, fmt.Errorf("tagged nodes are not users")
}
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
return nil, fmt.Errorf("failed to identify remote user")
}
return whois.UserProfile, nil
}

View File

@@ -9,7 +9,6 @@ import (
"errors"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
@@ -28,7 +27,7 @@ func runBugReport(ctx context.Context, args []string) error {
default:
return errors.New("unknown argumets")
}
logMarker, err := tailscale.BugReport(ctx, note)
logMarker, err := localClient.BugReport(ctx, note)
if err != nil {
return err
}

View File

@@ -50,7 +50,7 @@ func runCert(ctx context.Context, args []string) error {
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
if v, ok := localClient.ExpandSNIName(r.Context(), r.Host); ok {
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
return
}
@@ -64,7 +64,7 @@ func runCert(ctx context.Context, args []string) error {
if len(args) != 1 {
var hint bytes.Buffer
if st, err := tailscale.Status(ctx); err == nil {
if st, err := localClient.Status(ctx); err == nil {
if st.BackendState != ipn.Running.String() {
fmt.Fprintf(&hint, "\nTailscale is not running.\n")
} else if len(st.CertDomains) == 0 {

View File

@@ -25,6 +25,7 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
@@ -124,23 +125,13 @@ func CleanUpArgs(args []string) []string {
return out
}
var localClient tailscale.LocalClient
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) (err error) {
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
args = []string{"version"}
}
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy &&
os.Getenv("GOKRAZY_FIRST_START") == "1" {
defer func() {
// Exit with 125 otherwise the CLI binary is restarted
// forever in a loop by the Gokrazy process supervisor.
// See https://gokrazy.org/userguide/process-interface/
if err != nil {
log.Println(err)
}
os.Exit(125)
}()
}
var warnOnce sync.Once
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
@@ -170,6 +161,8 @@ change in the future.
ipCmd,
statusCmd,
pingCmd,
ncCmd,
sshCmd,
versionCmd,
webCmd,
fileCmd,
@@ -183,6 +176,9 @@ change in the future.
for _, c := range rootCmd.Subcommands {
c.UsageFunc = usageFunc
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands, idTokenCmd)
}
// Don't advertise the debug command, but it exists.
if strSliceContains(args, "debug") {
@@ -199,10 +195,10 @@ change in the future.
return err
}
tailscale.TailscaledSocket = rootArgs.socket
localClient.Socket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
tailscale.TailscaledSocketSetExplicitly = true
localClient.UseSocketOnly = true
}
})

View File

@@ -659,6 +659,39 @@ func TestPrefsFromUpArgs(t *testing.T) {
NoSNAT: true,
},
},
{
name: "via_route_good",
goos: "linux",
args: upArgsT{
advertiseRoutes: "fd7a:115c:a1e0:b1a::bb:10.0.0.0/112",
netfilterMode: "off",
},
want: &ipn.Prefs{
WantRunning: true,
NoSNAT: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
},
},
},
{
name: "via_route_short_prefix",
goos: "linux",
args: upArgsT{
advertiseRoutes: "fd7a:115c:a1e0:b1a::/64",
netfilterMode: "off",
},
wantErr: "fd7a:115c:a1e0:b1a::/64 4-in-6 prefix must be at least a /96",
},
{
name: "via_route_short_reserved_siteid",
goos: "linux",
args: upArgsT{
advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
netfilterMode: "off",
},
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xff or less",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
@@ -21,9 +22,10 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
@@ -66,6 +68,11 @@ var debugCmd = &ffcli.Command{
Exec: runEnv,
ShortHelp: "print cmd/tailscale environment",
},
{
Name: "stat",
Exec: runStat,
ShortHelp: "stat a file",
},
{
Name: "hostinfo",
Exec: runHostinfo,
@@ -106,6 +113,11 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "via",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
},
}
@@ -142,7 +154,7 @@ func runDebug(ctx context.Context, args []string) error {
if out := debugArgs.cpuFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing CPU profile for %v seconds ...", debugArgs.cpuSec)
if v, err := tailscale.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
if v, err := localClient.Profile(ctx, "profile", debugArgs.cpuSec); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@@ -154,7 +166,7 @@ func runDebug(ctx context.Context, args []string) error {
if out := debugArgs.memFile; out != "" {
usedFlag = true // TODO(bradfitz): add "profile" subcommand
log.Printf("Capturing memory profile ...")
if v, err := tailscale.Profile(ctx, "heap", 0); err != nil {
if v, err := localClient.Profile(ctx, "heap", 0); err != nil {
return err
} else {
if err := writeProfile(out, v); err != nil {
@@ -166,7 +178,7 @@ func runDebug(ctx context.Context, args []string) error {
if debugArgs.file != "" {
usedFlag = true // TODO(bradfitz): add "file" subcommand
if debugArgs.file == "get" {
wfs, err := tailscale.WaitingFiles(ctx)
wfs, err := localClient.WaitingFiles(ctx)
if err != nil {
fatalf("%v\n", err)
}
@@ -177,9 +189,9 @@ func runDebug(ctx context.Context, args []string) error {
}
delete := strings.HasPrefix(debugArgs.file, "delete:")
if delete {
return tailscale.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
return localClient.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:"))
}
rc, size, err := tailscale.GetWaitingFile(ctx, debugArgs.file)
rc, size, err := localClient.GetWaitingFile(ctx, debugArgs.file)
if err != nil {
return err
}
@@ -214,7 +226,7 @@ var prefsArgs struct {
}
func runPrefs(ctx context.Context, args []string) error {
prefs, err := tailscale.GetPrefs(ctx)
prefs, err := localClient.GetPrefs(ctx)
if err != nil {
return err
}
@@ -248,7 +260,7 @@ func runWatchIPN(ctx context.Context, args []string) error {
}
func runDERPMap(ctx context.Context, args []string) error {
dm, err := tailscale.CurrentDERPMap(ctx)
dm, err := localClient.CurrentDERPMap(ctx)
if err != nil {
return fmt.Errorf(
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
@@ -265,7 +277,7 @@ func localAPIAction(action string) func(context.Context, []string) error {
if len(args) > 0 {
return errors.New("unexpected arguments")
}
return tailscale.DebugAction(ctx, action)
return localClient.DebugAction(ctx, action)
}
}
@@ -276,6 +288,28 @@ func runEnv(ctx context.Context, args []string) error {
return nil
}
func runStat(ctx context.Context, args []string) error {
for _, a := range args {
fi, err := os.Lstat(a)
if err != nil {
fmt.Printf("%s: %v\n", a, err)
continue
}
fmt.Printf("%s: %v, %v\n", a, fi.Mode(), fi.Size())
if fi.IsDir() {
ents, _ := os.ReadDir(a)
for i, ent := range ents {
if i == 25 {
fmt.Printf(" ...\n")
break
}
fmt.Printf(" - %s\n", ent.Name())
}
}
}
return nil
}
func runHostinfo(ctx context.Context, args []string) error {
hi := hostinfo.New()
j, _ := json.MarshalIndent(hi, "", " ")
@@ -284,7 +318,7 @@ func runHostinfo(ctx context.Context, args []string) error {
}
func runDaemonGoroutines(ctx context.Context, args []string) error {
goroutines, err := tailscale.Goroutines(ctx)
goroutines, err := localClient.Goroutines(ctx)
if err != nil {
return err
}
@@ -299,7 +333,7 @@ var metricsArgs struct {
func runDaemonMetrics(ctx context.Context, args []string) error {
last := map[string]int64{}
for {
out, err := tailscale.DaemonMetrics(ctx)
out, err := localClient.DaemonMetrics(ctx)
if err != nil {
return err
}
@@ -348,3 +382,46 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
time.Sleep(time.Second)
}
}
func runVia(ctx context.Context, args []string) error {
switch len(args) {
default:
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
case 1:
ipp, err := netaddr.ParseIPPrefix(args[0])
if err != nil {
return err
}
if !ipp.IP().Is6() {
return errors.New("with one argument, expect an IPv6 CIDR")
}
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
return errors.New("not a via route")
}
if ipp.Bits() < 96 {
return errors.New("short length, want /96 or more")
}
v4 := tsaddr.UnmapVia(ipp.IP())
a := ipp.IP().As16()
siteID := binary.BigEndian.Uint32(a[8:12])
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
case 2:
siteID, err := strconv.ParseUint(args[0], 0, 32)
if err != nil {
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
}
if siteID > 0xff {
return fmt.Errorf("site-id values over 255 are currently reserved")
}
ipp, err := netaddr.ParseIPPrefix(args[1])
if err != nil {
return err
}
via, err := tsaddr.MapVia(uint32(siteID), ipp)
if err != nil {
return err
}
fmt.Println(via)
}
return nil
}

View File

@@ -6,10 +6,10 @@ package cli
import (
"context"
"flag"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
)
@@ -18,7 +18,14 @@ var downCmd = &ffcli.Command{
ShortUsage: "down",
ShortHelp: "Disconnect from Tailscale",
Exec: runDown,
Exec: runDown,
FlagSet: newDownFlagSet(),
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
registerAcceptRiskFlag(downf)
return downf
}
func runDown(ctx context.Context, args []string) error {
@@ -26,7 +33,13 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
if isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
return err
}
}
st, err := localClient.Status(ctx)
if err != nil {
return fmt.Errorf("error fetching current status: %w", err)
}
@@ -34,7 +47,7 @@ func runDown(ctx context.Context, args []string) error {
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil
}
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,
},

View File

@@ -24,7 +24,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -157,7 +156,7 @@ func runCp(ctx context.Context, args []string) error {
if cpArgs.verbose {
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
}
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
err := localClient.PushFile(ctx, stableID, contentLength, name, fileContents)
if err != nil {
return err
}
@@ -173,7 +172,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
if err != nil {
return "", false, err
}
fts, err := tailscale.FileTargets(ctx)
fts, err := localClient.FileTargets(ctx)
if err != nil {
return "", false, err
}
@@ -194,7 +193,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
found := false
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
if st, err := localClient.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
for _, pip := range peer.TailscaleIPs {
if pip == ip {
@@ -261,7 +260,7 @@ func runCpTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
}
fts, err := tailscale.FileTargets(ctx)
fts, err := localClient.FileTargets(ctx)
if err != nil {
return err
}
@@ -318,6 +317,7 @@ var fileGetCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("get")
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory.
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
@@ -329,6 +329,7 @@ var fileGetCmd = &ffcli.Command{
var getArgs = struct {
wait bool
loop bool
verbose bool
conflict onConflict
}{conflict: skipOnExist}
@@ -358,7 +359,7 @@ func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error)
}
return nil, fmt.Errorf("failed to write; %w", err)
case overwriteExisting:
// remove the target file and create it anew so we don't fall for an
// remove the target file and create it anew so we don't fall for an
// attacker who symlinks a known target name to a file he wants changed.
if err = os.Remove(targetFile); err != nil {
return nil, fmt.Errorf("unable to remove target file: %w", err)
@@ -383,22 +384,75 @@ func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error)
}
func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targetFile string, size int64, err error) {
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
rc, size, err := localClient.GetWaitingFile(ctx, wf.Name)
if err != nil {
return "", 0, fmt.Errorf("opening inbox file %q: %w", wf.Name, err)
}
defer rc.Close()
f, err := openFileOrSubstitute(dir, wf.Name, getArgs.conflict)
if err != nil {
return "", 0, err
}
_, err = io.Copy(f, rc)
rc.Close()
if err != nil {
f.Close()
return "", 0, fmt.Errorf("failed to write %v: %v", f.Name(), err)
}
return f.Name(), size, f.Close()
}
func runFileGetOneBatch(ctx context.Context, dir string) []error {
var wfs []apitype.WaitingFile
var err error
var errs []error
for len(errs) == 0 {
wfs, err = localClient.WaitingFiles(ctx)
if err != nil {
errs = append(errs, fmt.Errorf("getting WaitingFiles: %w", err))
break
}
if len(wfs) != 0 || !(getArgs.wait || getArgs.loop) {
break
}
if getArgs.verbose {
printf("waiting for file...")
}
if err := waitForFile(ctx); err != nil {
errs = append(errs, err)
}
}
deleted := 0
for i, wf := range wfs {
if len(errs) > 100 {
// Likely, everything is broken.
// Don't try to receive any more files in this batch.
errs = append(errs, fmt.Errorf("too many errors in runFileGetOneBatch(). %d files unexamined", len(wfs)-i))
break
}
writtenFile, size, err := receiveFile(ctx, wf, dir)
if err != nil {
errs = append(errs, err)
continue
}
if getArgs.verbose {
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
}
if err = localClient.DeleteWaitingFile(ctx, wf.Name); err != nil {
errs = append(errs, fmt.Errorf("deleting %q from inbox: %v", wf.Name, err))
continue
}
deleted++
}
if deleted == 0 && len(wfs) > 0 {
// persistently stuck files are basically an error
errs = append(errs, fmt.Errorf("moved %d/%d files", deleted, len(wfs)))
} else if getArgs.verbose {
printf("moved %d/%d files\n", deleted, len(wfs))
}
return errs
}
func runFileGet(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: file get <target-directory>")
@@ -413,45 +467,28 @@ func runFileGet(ctx context.Context, args []string) error {
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
return fmt.Errorf("%q is not a directory", dir)
}
var wfs []apitype.WaitingFile
var err error
for {
wfs, err = tailscale.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %w", err)
}
if len(wfs) != 0 || !getArgs.wait {
break
}
if getArgs.verbose {
printf("waiting for file...")
}
if err := waitForFile(ctx); err != nil {
return err
if getArgs.loop {
for {
errs := runFileGetOneBatch(ctx, dir)
for _, err := range errs {
outln(err)
}
if len(errs) > 0 {
// It's possible whatever caused the error(s) (e.g. conflicting target file,
// full disk, unwritable target directory) will re-occur if we try again so
// let's back off and not busy loop on error.
//
// If we've been invoked as:
// tailscale file get --conflict=skip ~/Downloads
// then any file coming in named the same as one in ~/Downloads will always
// appear as an "error" until the user clears it, but other incoming files
// should be receivable when they arrive, so let's not wait too long to
// check again.
time.Sleep(5 * time.Second)
}
}
}
var errs []error
deleted := 0
for _, wf := range wfs {
writtenFile, size, err := receiveFile(ctx, wf, dir)
if err != nil {
errs = append(errs, err)
continue
}
if getArgs.verbose {
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
}
if err = tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
errs = append(errs, fmt.Errorf("deleting %q from inbox: %v", wf.Name, err))
continue
}
deleted++
}
if getArgs.verbose {
printf("moved %d/%d files\n", deleted, len(wfs))
}
errs := runFileGetOneBatch(ctx, dir)
if len(errs) == 0 {
return nil
}
@@ -465,7 +502,7 @@ func wipeInbox(ctx context.Context) error {
if getArgs.wait {
return errors.New("can't use --wait with /dev/null target")
}
wfs, err := tailscale.WaitingFiles(ctx)
wfs, err := localClient.WaitingFiles(ctx)
if err != nil {
return fmt.Errorf("getting WaitingFiles: %w", err)
}
@@ -474,7 +511,7 @@ func wipeInbox(ctx context.Context) error {
if getArgs.verbose {
log.Printf("deleting %v ...", wf.Name)
}
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
if err := localClient.DeleteWaitingFile(ctx, wf.Name); err != nil {
return fmt.Errorf("deleting %q: %v", wf.Name, err)
}
deleted++
@@ -489,9 +526,10 @@ 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 {
fatalf("Notify.ErrMessage: %v\n", *n.ErrMessage)
notifyError <- fmt.Errorf("Notify.ErrMessage: %v", *n.ErrMessage)
}
if n.FilesWaiting != nil {
select {
@@ -508,5 +546,7 @@ func waitForFile(ctx context.Context) error {
return pumpCtx.Err()
case <-ctx.Done():
return ctx.Err()
case err := <-notifyError:
return err
}
}

View File

@@ -0,0 +1,34 @@
// 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"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
)
var idTokenCmd = &ffcli.Command{
Name: "id-token",
ShortUsage: "id-token <aud>",
ShortHelp: "fetch an OIDC id-token for the Tailscale machine",
Exec: runIDToken,
}
func runIDToken(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: id-token <aud>")
}
tr, err := localClient.IDToken(ctx, args[0])
if err != nil {
return err
}
fmt.Println(tr.IDToken)
return nil
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn/ipnstate"
)
@@ -59,7 +58,7 @@ func runIP(ctx context.Context, args []string) error {
if !v4 && !v6 {
v4, v6 = true, true
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return err
}

View File

@@ -10,7 +10,6 @@ import (
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var logoutCmd = &ffcli.Command{
@@ -30,5 +29,5 @@ func runLogout(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
return tailscale.Logout(ctx)
return localClient.Logout(ctx)
}

62
cmd/tailscale/cli/nc.go Normal file
View File

@@ -0,0 +1,62 @@
// 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"
"fmt"
"io"
"os"
"strconv"
"github.com/peterbourgon/ff/v3/ffcli"
)
var ncCmd = &ffcli.Command{
Name: "nc",
ShortUsage: "nc <hostname-or-IP> <port>",
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
Exec: runNC,
}
func runNC(ctx context.Context, args []string) error {
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
if len(args) != 2 {
return errors.New("usage: nc <hostname-or-IP> <port>")
}
hostOrIP, portStr := args[0], args[1]
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return fmt.Errorf("invalid port number %q", portStr)
}
// TODO(bradfitz): also add UDP too, via flag?
c, err := localClient.DialTCP(ctx, hostOrIP, uint16(port))
if err != nil {
return fmt.Errorf("Dial(%q, %v): %w", hostOrIP, port, err)
}
defer c.Close()
errc := make(chan error, 1)
go func() {
_, err := io.Copy(os.Stdout, c)
errc <- err
}()
go func() {
_, err := io.Copy(c, os.Stdin)
errc <- err
}()
return <-errc
}

View File

@@ -18,7 +18,6 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
@@ -63,7 +62,7 @@ func runNetcheck(ctx context.Context, args []string) error {
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm, err := tailscale.CurrentDERPMap(ctx)
dm, err := localClient.CurrentDERPMap(ctx)
noRegions := dm != nil && len(dm.Regions) == 0
if noRegions {
log.Printf("No DERP map from tailscaled; using default.")

View File

@@ -16,9 +16,9 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"inet.af/netaddr"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
var pingCmd = &ffcli.Command{
@@ -27,7 +27,7 @@ var pingCmd = &ffcli.Command{
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
LongHelp: strings.TrimSpace(`
The 'tailscale ping' command pings a peer node at the Tailscale layer
The 'tailscale ping' command pings a peer node from the Tailscale layer
and reports which route it took for each response. The first ping or
so will likely go over DERP (Tailscale's TCP relay protocol) while NAT
traversal finds a direct path through.
@@ -49,7 +49,9 @@ relay node.
fs := newFlagSet("ping")
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
return fs
@@ -61,11 +63,26 @@ var pingArgs struct {
untilDirect bool
verbose bool
tsmp bool
icmp bool
peerAPI bool
timeout time.Duration
}
func pingType() tailcfg.PingType {
if pingArgs.tsmp {
return tailcfg.PingTSMP
}
if pingArgs.icmp {
return tailcfg.PingICMP
}
if pingArgs.peerAPI {
return tailcfg.PingPeerAPI
}
return tailcfg.PingDisco
}
func runPing(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@@ -75,24 +92,10 @@ func runPing(ctx context.Context, args []string) error {
os.Exit(1)
}
c, bc, ctx, cancel := connect(ctx)
defer cancel()
if len(args) != 1 || args[0] == "" {
return errors.New("usage: ping <hostname-or-IP>")
}
var ip string
prc := make(chan *ipnstate.PingResult, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
fatalf("Notify.ErrMessage: %v", *n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
prc <- pr
}
})
pumpErr := make(chan error, 1)
go func() { pumpErr <- pump(ctx, bc, c) }()
hostOrIP := args[0]
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
@@ -112,48 +115,51 @@ func runPing(ctx context.Context, args []string) error {
anyPong := false
for {
n++
bc.Ping(ip, pingArgs.tsmp)
timer := time.NewTimer(pingArgs.timeout)
select {
case <-timer.C:
printf("timeout waiting for ping reply\n")
case err := <-pumpErr:
ctx, cancel := context.WithTimeout(ctx, pingArgs.timeout)
pr, err := localClient.Ping(ctx, netaddr.MustParseIP(ip), pingType())
cancel()
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
printf("ping %q timed out\n", ip)
continue
}
return err
case pr := <-prc:
timer.Stop()
if pr.Err != "" {
if pr.IsLocalIP {
outln(pr.Err)
return nil
}
return errors.New(pr.Err)
}
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
via := pr.Endpoint
if pr.DERPRegionID != 0 {
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
}
if pingArgs.tsmp {
// TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
// For now just say it came via TSMP.
via = "TSMP"
}
anyPong = true
extra := ""
if pr.PeerAPIPort != 0 {
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
}
printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
if pingArgs.tsmp {
return nil
}
if pr.Endpoint != "" && pingArgs.untilDirect {
return nil
}
time.Sleep(time.Second)
case <-ctx.Done():
return ctx.Err()
}
if pr.Err != "" {
if pr.IsLocalIP {
outln(pr.Err)
return nil
}
return errors.New(pr.Err)
}
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
via := pr.Endpoint
if pr.DERPRegionID != 0 {
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
}
if via == "" {
// TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
// For now just say which protocol it used.
via = string(pingType())
}
if pingArgs.peerAPI {
printf("hit peerapi of %s (%s) at %s in %s\n", pr.NodeIP, pr.NodeName, pr.PeerAPIURL, latency)
return nil
}
anyPong = true
extra := ""
if pr.PeerAPIPort != 0 {
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
}
printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
if pingArgs.tsmp || pingArgs.icmp {
return nil
}
if pr.Endpoint != "" && pingArgs.untilDirect {
return nil
}
time.Sleep(time.Second)
if n == pingArgs.num {
if !anyPong {
return errors.New("no reply")
@@ -173,7 +179,7 @@ func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, self b
}
// Otherwise, try to resolve it first from the network peer list.
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return "", false, err
}

View File

@@ -0,0 +1,78 @@
// 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 (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
var (
riskTypes []string
acceptedRisks string
riskLoseSSH = registerRiskType("lose-ssh")
)
func registerRiskType(riskType string) string {
riskTypes = append(riskTypes, riskType)
return riskType
}
// 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, ","))
}
// riskAccepted reports whether riskType is in acceptedRisks.
func riskAccepted(riskType string) bool {
for _, r := range strings.Split(acceptedRisks, ",") {
if r == riskType {
return true
}
}
return false
}
var errAborted = errors.New("aborted, no changes made")
// riskAbortTimeSeconds is the number of seconds to wait after displaying the
// risk message before continuing with the operation.
// 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) {
return nil
}
fmt.Println(riskMessage)
fmt.Printf("To skip this warning, use --accept-risk=%s\n", riskType)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT)
var msgLen int
for left := riskAbortTimeSeconds; left > 0; left-- {
msg := fmt.Sprintf("\rContinuing in %d seconds...", left)
msgLen = len(msg)
fmt.Print(msg)
select {
case <-interrupt:
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen+1))
return errAborted
case <-time.After(time.Second):
continue
}
}
fmt.Printf("\r%s\r", strings.Repeat(" ", msgLen))
return errAborted
}

211
cmd/tailscale/cli/ssh.go Normal file
View File

@@ -0,0 +1,211 @@
// 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"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
"tailscale.com/version"
)
var sshCmd = &ffcli.Command{
Name: "ssh",
ShortUsage: "ssh [user@]<host> [args...]",
ShortHelp: "SSH to a Tailscale machine",
Exec: runSSH,
}
func runSSH(ctx context.Context, args []string) error {
if runtime.GOOS == "darwin" && version.IsSandboxedMacOS() && !envknob.UseWIPCode() {
return errors.New("The 'tailscale ssh' subcommand is not available on sandboxed macOS builds.\nUse the regular 'ssh' client instead.")
}
if len(args) == 0 {
return errors.New("usage: ssh [user@]<host>")
}
arg, argRest := args[0], args[1:]
username, host, ok := strings.Cut(arg, "@")
if !ok {
host = arg
lu, err := user.Current()
if err != nil {
return nil
}
username = lu.Username
}
st, err := localClient.Status(ctx)
if err != nil {
return err
}
// hostForSSH is the hostname we'll tell OpenSSH we're
// connecting to, so we have to maintain fewer entries in the
// known_hosts files.
hostForSSH := host
if v, ok := nodeDNSNameFromArg(st, host); ok {
hostForSSH = v
}
ssh, err := exec.LookPath("ssh")
if err != nil {
// TODO(bradfitz): use Go's crypto/ssh client instead
// of failing. But for now:
return fmt.Errorf("no system 'ssh' command found: %w", err)
}
tailscaleBin, err := os.Executable()
if err != nil {
return err
}
knownHostsFile, err := writeKnownHosts(st)
if err != nil {
return err
}
argv := []string{ssh}
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
argv = append(argv, "-vvv")
}
argv = append(argv,
// Only trust SSH hosts that we know about.
"-o", fmt.Sprintf("UserKnownHostsFile %q", knownHostsFile),
"-o", "UpdateHostKeys no",
"-o", "StrictHostKeyChecking yes",
)
// TODO(bradfitz): nc is currently broken on macOS:
// https://github.com/tailscale/tailscale/issues/4529
// 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" {
argv = append(argv,
"-o", fmt.Sprintf("ProxyCommand %q --socket=%q nc %%h %%p",
tailscaleBin,
rootArgs.socket,
))
}
// Explicitly rebuild the user@host argument rather than
// passing it through. In general, the use of OpenSSH's ssh
// binary is a crutch for now. We don't want to be
// Hyrum-locked into passing through all OpenSSH flags to the
// OpenSSH client forever. We try to make our flags and args
// be compatible, but only a subset. The "tailscale ssh"
// command should be a simple and portable one. If they want
// to use a different one, we'll later be making stock ssh
// work well by default too. (doing things like automatically
// setting known_hosts, etc)
argv = append(argv, username+"@"+hostForSSH)
argv = append(argv, argRest...)
if envknob.Bool("TS_DEBUG_SSH_EXEC") {
log.Printf("Running: %q, %q ...", ssh, argv)
}
return execSSH(ssh, argv)
}
func writeKnownHosts(st *ipnstate.Status) (knownHostsFile string, err error) {
confDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
tsConfDir := filepath.Join(confDir, "tailscale")
if err := os.MkdirAll(tsConfDir, 0700); err != nil {
return "", err
}
knownHostsFile = filepath.Join(tsConfDir, "ssh_known_hosts")
want := genKnownHosts(st)
if cur, err := os.ReadFile(knownHostsFile); err != nil || !bytes.Equal(cur, want) {
if err := os.WriteFile(knownHostsFile, want, 0644); err != nil {
return "", err
}
}
return knownHostsFile, nil
}
func genKnownHosts(st *ipnstate.Status) []byte {
var buf bytes.Buffer
for _, k := range st.Peers() {
ps := st.Peer[k]
for _, hk := range ps.SSH_HostKeys {
hostKey := strings.TrimSpace(hk)
if strings.ContainsAny(hostKey, "\n\r") { // invalid
continue
}
fmt.Fprintf(&buf, "%s %s\n", ps.DNSName, hostKey)
}
}
return buf.Bytes()
}
// nodeDNSNameFromArg returns the PeerStatus.DNSName value from a peer
// in st that matches the input arg which can be a base name, full
// DNS name, or an IP.
func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok bool) {
if arg == "" {
return
}
argIP, _ := netaddr.ParseIP(arg)
for _, ps := range st.Peer {
dnsName = ps.DNSName
if !argIP.IsZero() {
for _, ip := range ps.TailscaleIPs {
if ip == argIP {
return dnsName, true
}
}
continue
}
if strings.EqualFold(strings.TrimSuffix(arg, "."), strings.TrimSuffix(dnsName, ".")) {
return dnsName, true
}
if base, _, ok := strings.Cut(ps.DNSName, "."); ok && strings.EqualFold(base, arg) {
return dnsName, true
}
}
return "", false
}
// getSSHClientEnvVar returns the "SSH_CLIENT" environment variable
// for the current process group, if any.
var getSSHClientEnvVar = func() string {
return ""
}
// isSSHOverTailscale checks if the invocation is in a SSH session over Tailscale.
// It is used to detect if the user is about to take an action that might result in them
// disconnecting from the machine (e.g. disabling SSH)
func isSSHOverTailscale() bool {
sshClient := getSSHClientEnvVar()
if sshClient == "" {
return false
}
ipStr, _, ok := strings.Cut(sshClient, " ")
if !ok {
return false
}
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return false
}
return tsaddr.IsTailscaleIP(ip)
}

View File

@@ -0,0 +1,21 @@
// 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 !js && !windows
// +build !js,!windows
package cli
import (
"errors"
"os"
"syscall"
)
func execSSH(ssh string, argv []string) error {
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
return err
}
return errors.New("unreachable")
}

View File

@@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !hermetic
// +build !hermetic
package cli
package dnsfallback
import (
"errors"
)
//go:generate go run update-dns-fallbacks.go
func execSSH(ssh string, argv []string) error {
return errors.New("Not implemented")
}

View File

@@ -0,0 +1,25 @@
// 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 (
"errors"
"os"
"os/exec"
)
func execSSH(ssh string, argv []string) error {
// Don't use syscall.Exec on Windows, it's not fully implemented.
cmd := exec.Command(ssh, argv[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
var ee *exec.ExitError
err := cmd.Run()
if errors.As(err, &ee) {
os.Exit(ee.ExitCode())
}
return err
}

View File

@@ -0,0 +1,51 @@
// 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 !js && !windows
// +build !js,!windows
package cli
import (
"bytes"
"os"
"path/filepath"
"runtime"
"strconv"
"golang.org/x/sys/unix"
)
func init() {
getSSHClientEnvVar = func() string {
if os.Getenv("SUDO_USER") == "" {
// No sudo, just check the env.
return os.Getenv("SSH_CLIENT")
}
if runtime.GOOS != "linux" {
// TODO(maisem): implement this for other platforms. It's not clear
// if there is a way to get the environment for a given process on
// darwin and bsd.
return ""
}
// SID is the session ID of the user's login session.
// It is also the process ID of the original shell that the user logged in with.
// We only need to check the environment of that process.
sid, err := unix.Getsid(os.Getpid())
if err != nil {
return ""
}
b, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(sid), "environ"))
if err != nil {
return ""
}
prefix := []byte("SSH_CLIENT=")
for _, env := range bytes.Split(b, []byte{0}) {
if bytes.HasPrefix(env, prefix) {
return string(env[len(prefix):])
}
}
return ""
}
}

View File

@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"net"
@@ -18,7 +19,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
@@ -69,7 +69,14 @@ var statusArgs struct {
}
func runStatus(ctx context.Context, args []string) error {
st, err := tailscale.Status(ctx)
if len(args) > 0 {
return errors.New("unexpected non-flag arguments to 'tailscale status'")
}
getStatus := localClient.Status
if !statusArgs.peers {
getStatus = localClient.StatusWithoutPeers
}
st, err := getStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@@ -107,7 +114,7 @@ func runStatus(ctx context.Context, args []string) error {
http.NotFound(w, r)
return
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -121,12 +128,8 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
os.Exit(1)
}
// print health check information prior to checking LocalBackend state as
// it may provide an explanation to the user if we choose to exit early
if len(st.Health) > 0 {
printf("# Health check:\n")
for _, m := range st.Health {
@@ -135,6 +138,12 @@ func runStatus(ctx context.Context, args []string) error {
outln()
}
description, ok := isRunningOrStarting(st)
if !ok {
outln(description)
os.Exit(1)
}
var buf bytes.Buffer
f := func(format string, a ...any) { fmt.Fprintf(&buf, format, a...) }
printPS := func(ps *ipnstate.PeerStatus) {

View File

@@ -7,6 +7,7 @@ package cli
import (
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"flag"
@@ -18,15 +19,15 @@ import (
"sort"
"strings"
"sync"
"time"
shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
@@ -51,7 +52,7 @@ down").
If flags are specified, the flags must be the complete set of desired
settings. An error is returned if any setting would be changed as a
result of an unspecified flag's default value, unless the --reset flag
is also used. (The flags --authkey, --force-reauth, and --qr are not
is also used. (The flags --auth-key, --force-reauth, and --qr are not
considered settings that need to be re-specified when modifying
settings.)
`),
@@ -98,9 +99,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
if envknob.UseWIPCode() || inTest() {
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
}
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
@@ -116,6 +115,8 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
case "windows":
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
registerAcceptRiskFlag(upf)
return upf
}
@@ -148,6 +149,7 @@ type upArgsT struct {
hostname string
opUser string
json bool
timeout time.Duration
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -201,6 +203,26 @@ var (
ipv6default = netaddr.MustParseIPPrefix("::/0")
)
func validateViaPrefix(ipp netaddr.IPPrefix) error {
if !tsaddr.IsViaPrefix(ipp) {
return fmt.Errorf("%v is not a 4-in-6 prefix", ipp)
}
if ipp.Bits() < (128 - 32) {
return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32)
}
a := ipp.IP().As16()
// The first 64 bits of a are the via prefix.
// The next 32 bits are the "site ID".
// The last 32 bits are the IPv4.
// For now, we reserve the top 3 bytes of the site ID,
// and only allow users to use site IDs 0-255.
siteID := binary.BigEndian.Uint32(a[8:12])
if siteID > 0xFF {
return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xff or less", ipp, siteID)
}
return nil
}
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
routeMap := map[netaddr.IPPrefix]bool{}
if advertiseRoutes != "" {
@@ -214,6 +236,11 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
if ipp != ipp.Masked() {
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
if tsaddr.IsViaPrefix(ipp) {
if err := validateViaPrefix(ipp); err != nil {
return nil, err
}
}
if ipp == ipv4default {
default4 = true
} else if ipp == ipv6default {
@@ -382,7 +409,7 @@ func runUp(ctx context.Context, args []string) error {
fatalf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
@@ -423,12 +450,12 @@ func runUp(ctx context.Context, args []string) error {
}
if len(prefs.AdvertiseRoutes) > 0 {
if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
if err := localClient.CheckIPForwarding(context.Background()); err != nil {
warnf("%v", err)
}
}
curPrefs, err := tailscale.GetPrefs(ctx)
curPrefs, err := localClient.GetPrefs(ctx)
if err != nil {
return err
}
@@ -442,12 +469,24 @@ func runUp(ctx context.Context, args []string) error {
backendState: st.BackendState,
curExitNodeIP: exitNodeIP(curPrefs, st),
}
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
if upArgs.runSSH {
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
} else {
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
}
if err != nil {
return err
}
}
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil {
fatalf("%s", err)
}
if justEditMP != nil {
_, err := tailscale.EditPrefs(ctx, justEditMP)
_, err := localClient.EditPrefs(ctx, justEditMP)
return err
}
@@ -558,7 +597,7 @@ func runUp(ctx context.Context, args []string) error {
// Special case: bare "tailscale up" means to just start
// running, if there's ever been a login.
if simpleUp {
_, err := tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
},
@@ -568,6 +607,10 @@ func runUp(ctx context.Context, args []string) error {
return err
}
} else {
if err := localClient.CheckPrefs(ctx, prefs); err != nil {
return err
}
authKey, err := upArgs.getAuthKey()
if err != nil {
return err
@@ -605,6 +648,12 @@ func runUp(ctx context.Context, args []string) error {
// need to prioritize reads from 'running' if it's
// readable; its send does happen before the pump mechanism
// shuts down. (Issue 2333)
var timeoutCh <-chan time.Time
if upArgs.timeout > 0 {
timeoutTimer := time.NewTimer(upArgs.timeout)
defer timeoutTimer.Stop()
timeoutCh = timeoutTimer.C
}
select {
case <-running:
return nil
@@ -622,6 +671,8 @@ func runUp(ctx context.Context, args []string) error {
default:
}
return err
case <-timeoutCh:
return errors.New(`timeout waiting for Tailscale service to enter a Running state; check health with "tailscale status"`)
}
}
@@ -678,7 +729,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
// correspond to an ipn.Pref.
func preflessFlag(flagName string) bool {
switch flagName {
case "auth-key", "force-reauth", "reset", "qr", "json":
case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk":
return true
}
return false

View File

@@ -10,7 +10,6 @@ import (
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/version"
)
@@ -41,7 +40,7 @@ func runVersion(ctx context.Context, args []string) error {
printf("Client: %s\n", version.String())
st, err := tailscale.StatusWithoutPeers(ctx)
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return err
}

View File

@@ -28,7 +28,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
@@ -269,15 +268,14 @@ func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
}
// We need a SynoToken for authenticate.cgi.
// So we tell the client to get one.
serverURL := r.URL.Scheme + "://" + r.URL.Host
synoTokenRedirectHTML.Execute(w, serverURL)
_, _ = fmt.Fprint(w, synoTokenRedirectHTML)
return true
}
var synoTokenRedirectHTML = template.Must(template.New("redirect").Parse(`<html><body>
const synoTokenRedirectHTML = `<html><body>
Redirecting with session token...
<script>
var serverURL = {{ . }};
var serverURL = window.location.protocol + "//" + window.location.host;
var req = new XMLHttpRequest();
req.overrideMimeType("application/json");
req.open("GET", serverURL + "/webman/login.cgi", true);
@@ -289,7 +287,7 @@ req.onload = function() {
req.send(null);
</script>
</body></html>
`))
`
func webHandler(w http.ResponseWriter, r *http.Request) {
if authRedirect(w, r) {
@@ -319,7 +317,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(mi{"error": err.Error()})
return
}
prefs, err := tailscale.GetPrefs(r.Context())
prefs, err := localClient.GetPrefs(r.Context())
if err != nil && !postData.Reauthenticate {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(mi{"error": err.Error()})
@@ -349,12 +347,12 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
return
}
st, err := tailscale.Status(r.Context())
st, err := localClient.Status(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
prefs, err := tailscale.GetPrefs(r.Context())
prefs, err := localClient.GetPrefs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -407,7 +405,7 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
prefs.NetfilterMode = preftype.NetfilterOff
}
st, err := tailscale.Status(ctx)
st, err := localClient.Status(ctx)
if err != nil {
return "", fmt.Errorf("can't fetch status: %v", err)
}

View File

@@ -42,7 +42,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
tailscale.com/disco from tailscale.com/derp
tailscale.com/envknob from tailscale.com/cmd/tailscale/cli+
tailscale.com/hostinfo from tailscale.com/net/interfaces+
@@ -56,13 +55,15 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
💣 tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
L tailscale.com/net/wsconn from tailscale.com/derp/derphttp
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
@@ -87,8 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/lineread from tailscale.com/net/interfaces+
W tailscale.com/util/winutil from tailscale.com/hostinfo
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/filter from tailscale.com/types/netmap
@@ -192,7 +192,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
os from crypto/rand+
os/exec from github.com/toqueteos/webbrowser+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from tailscale.com/util/groupmember
os/user from tailscale.com/util/groupmember+
path from html/template+
path/filepath from crypto/x509+
reflect from crypto/x509+

View File

@@ -3,7 +3,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
LD github.com/anmitsu/go-shlex from github.com/tailscale/ssh
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
@@ -76,12 +76,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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 from github.com/klauspost/compress/zstd
L github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
LD github.com/kr/fs from github.com/pkg/sftp
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
@@ -89,7 +92,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
W github.com/pkg/errors from github.com/tailscale/certstore
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@@ -97,7 +106,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
LD github.com/tailscale/ssh from tailscale.com/ssh/tailssh
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
@@ -106,7 +114,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/client/tailscale+
💣 go4.org/mem from tailscale.com/control/controlbase+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
@@ -127,15 +135,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/refsvfs2+
gvisor.dev/gvisor/pkg/refsvfs2 from gvisor.dev/gvisor/pkg/tcpip/stack+
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -148,44 +156,43 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/internal/network+
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/raw+
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/udp+
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
inet.af/netaddr from inet.af/wf+
inet.af/netaddr from tailscale.com/control/controlclient+
inet.af/peercred from tailscale.com/ipn/ipnserver
W 💣 inet.af/wf from tailscale.com/wf
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
tailscale.com from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
@@ -196,40 +203,43 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/control/controlclient+
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/filch from tailscale.com/logpolicy
💣 tailscale.com/metrics from tailscale.com/derp+
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netknob from tailscale.com/logpolicy+
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
tailscale.com/net/netknob from tailscale.com/net/netns+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
💣 tailscale.com/paths from tailscale.com/client/tailscale+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/net/dns+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
@@ -239,8 +249,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/cmd/tailscaled+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/key from tailscale.com/control/controlbase+
tailscale.com/types/logger from tailscale.com/control/controlclient+
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
@@ -249,31 +259,31 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/cmd/tailscaled+
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
LW tailscale.com/util/endian from tailscale.com/net/dns+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
tailscale.com/version from tailscale.com/client/tailscale+
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
tailscale.com/wgengine/wglog from tailscale.com/wgengine
@@ -281,19 +291,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
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/poly1305 from golang.zx2c4.com/wireguard/device
golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from github.com/tailscale/ssh+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
@@ -311,8 +321,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+
W golang.org/x/sys/windows from github.com/go-ole/go-ole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
@@ -344,10 +355,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/sha256 from crypto/tls+
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
embed from crypto/elliptic+
embed from tailscale.com+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -355,19 +366,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
encoding/xml from github.com/tailscale/goupnp+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
flag from tailscale.com/control/controlclient+
fmt from compress/flate+
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem
html from net/http/pprof+
html from tailscale.com/ipn/ipnlocal+
io from bufio+
io/fs from crypto/rand+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+
@@ -384,19 +395,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/http/internal from net/http+
net/http/pprof from tailscale.com/cmd/tailscaled+
net/netip from golang.zx2c4.com/wireguard/conn+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscaled+
os/user from github.com/godbus/dbus/v5+
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
path from github.com/godbus/dbus/v5+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+
runtime/debug from golang.org/x/sync/singleflight+
runtime/pprof from tailscale.com/log/logheap+
runtime/trace from net/http/pprof
sort from compress/flate+
strconv from compress/flate+

11
cmd/tailscaled/ssh.go Normal file
View File

@@ -0,0 +1,11 @@
// 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 || darwin
// +build linux darwin
package main
// Force registration of tailssh with LocalBackend.
import _ "tailscale.com/ssh/tailssh"

View File

@@ -25,7 +25,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"syscall"
"time"
@@ -130,14 +129,6 @@ var subCommands = map[string]*func([]string) error{
}
func main() {
// We aren't very performance sensitive, and the parts that are
// performance sensitive (wireguard) try hard not to do any memory
// allocations. So let's be aggressive about garbage collection,
// unless the user specifically overrides it in the usual way.
if _, ok := os.LookupEnv("GOGC"); !ok {
debug.SetGCPercent(10)
}
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
@@ -146,7 +137,7 @@ func main() {
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
@@ -167,13 +158,12 @@ func main() {
}
}
if beWindowsSubprocess() {
return
}
flag.Parse()
if flag.NArg() > 0 {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
// Windows subprocess is spawned with /subprocess, so we need to avoid this check there.
if runtime.GOOS != "windows" || flag.Arg(0) != "/subproc" {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
}
}
if printVersion {
@@ -181,9 +171,6 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && runtime.Version() == "go1.18" {
log.Fatalf("tailscaled is broken on macOS with go1.18 due to upstream bug https://github.com/golang/go/issues/51759; use 1.18.1+ or Tailscale's Go fork")
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
@@ -199,6 +186,16 @@ func main() {
log.Fatalf("--bird-socket is not supported on %s", runtime.GOOS)
}
// Only apply a default statepath when neither have been provided, so that a
// user may specify only --statedir if they wish.
if args.statepath == "" && args.statedir == "" {
args.statepath = paths.DefaultTailscaledStateFile()
}
if beWindowsSubprocess() {
return
}
err := run()
// Remove file sharing from Windows shell (noop in non-windows)
@@ -344,6 +341,7 @@ func run() error {
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := new(tsdial.Dialer) // mutated below (before used)
dialer.Logf = logf
e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return fmt.Errorf("createEngine: %w", err)
@@ -353,7 +351,7 @@ func run() error {
}
if debugMux != nil {
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, ok := ig.GetInternals(); ok {
if _, mc, _, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
}
@@ -406,6 +404,7 @@ func run() error {
// want to keep running.
signal.Ignore(syscall.SIGPIPE)
go func() {
defer dialer.Close()
select {
case s := <-interrupt:
logf("tailscaled got signal %v; shutting down", s)
@@ -576,11 +575,11 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
tunDev, magicConn, dns, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn, dialer)
return netstack.Create(logf, tunDev, e, magicConn, dialer, dns)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -30,6 +30,7 @@ import (
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/envknob"
@@ -60,12 +61,31 @@ func isWindowsService() bool {
return v
}
// syslogf is a logger function that writes to the Windows event log (ie, the
// one that you see in the Windows Event Viewer). tailscaled may optionally
// generate diagnostic messages in the same event timeline as the Windows
// Service Control Manager to assist with diagnosing issues with tailscaled's
// lifetime (such as slow shutdowns).
var syslogf logger.Logf = logger.Discard
// runWindowsService starts running Tailscale under the Windows
// Service environment.
//
// At this point we're still the parent process that
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {
syslog, err := eventlog.Open(serviceName)
if err == nil {
syslogf = func(format string, args ...any) {
syslog.Info(0, fmt.Sprintf(format, args...))
}
defer syslog.Close()
}
}
syslogf("Service entering svc.Run")
defer syslogf("Service exiting svc.Run")
return svc.Run(serviceName, &ipnService{Policy: pol})
}
@@ -75,7 +95,10 @@ type ipnService struct {
// Called by Windows to execute the windows service.
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
defer syslogf("SvcStopped notification imminent")
changes <- svc.Status{State: svc.StartPending}
syslogf("Service start pending")
svcAccepts := svc.AcceptStop
if winutil.GetPolicyInteger("FlushDNSOnSessionUnlock", 0) != 0 {
@@ -98,26 +121,29 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
}()
changes <- svc.Status{State: svc.Running, Accepts: svcAccepts}
syslogf("Service running")
for ctx.Err() == nil {
for {
select {
case <-doneCh:
return false, windows.NO_ERROR
case cmd := <-r:
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
switch cmd.Cmd {
case svc.Stop:
cancel()
changes <- svc.Status{State: svc.StopPending}
syslogf("Service stop pending")
cancel() // so BabysitProc will kill the child process
case svc.Interrogate:
syslogf("Service interrogation")
changes <- cmd.CurrentStatus
case svc.SessionChange:
syslogf("Service session change notification")
handleSessionChange(cmd)
changes <- cmd.CurrentStatus
}
}
}
changes <- svc.Status{State: svc.StopPending}
return false, windows.NO_ERROR
}
func cmdName(c svc.Cmd) string {
@@ -234,19 +260,19 @@ func startIPNServer(ctx context.Context, logid string) error {
linkMon, err := monitor.New(logf)
if err != nil {
return err
return fmt.Errorf("monitor: %w", err)
}
dialer := new(tsdial.Dialer)
getEngineRaw := func() (wgengine.Engine, error) {
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
return nil, fmt.Errorf("TUN: %w", err)
return nil, nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev, nil)
if err != nil {
dev.Close()
return nil, fmt.Errorf("router: %w", err)
return nil, nil, fmt.Errorf("router: %w", err)
}
if wrapNetstack {
r = netstack.NewSubnetRouterWrapper(r)
@@ -255,7 +281,7 @@ func startIPNServer(ctx context.Context, logid string) error {
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("DNS: %w", err)
return nil, nil, fmt.Errorf("DNS: %w", err)
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
@@ -268,23 +294,24 @@ func startIPNServer(ctx context.Context, logid string) error {
if err != nil {
r.Close()
dev.Close()
return nil, fmt.Errorf("engine: %w", err)
return nil, nil, fmt.Errorf("engine: %w", err)
}
ns, err := newNetstack(logf, dialer, eng)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
return nil, nil, fmt.Errorf("newNetstack: %w", err)
}
ns.ProcessLocalIPs = false
ns.ProcessSubnets = wrapNetstack
if err := ns.Start(); err != nil {
return nil, fmt.Errorf("failed to start netstack: %w", err)
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
}
return wgengine.NewWatchdog(eng), nil
return wgengine.NewWatchdog(eng), ns, nil
}
type engineOrError struct {
Engine wgengine.Engine
Err error
Engine wgengine.Engine
Netstack *netstack.Impl
Err error
}
engErrc := make(chan engineOrError)
t0 := time.Now()
@@ -293,7 +320,7 @@ func startIPNServer(ctx context.Context, logid string) error {
for try := 1; ; try++ {
logf("tailscaled: getting engine... (try %v)", try)
t1 := time.Now()
eng, err := getEngineRaw()
eng, ns, err := getEngineRaw()
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
if err != nil {
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
@@ -306,7 +333,7 @@ func startIPNServer(ctx context.Context, logid string) error {
}
}
timer := time.NewTimer(5 * time.Second)
engErrc <- engineOrError{eng, err}
engErrc <- engineOrError{eng, ns, err}
if err == nil {
timer.Stop()
return
@@ -318,14 +345,14 @@ func startIPNServer(ctx context.Context, logid string) error {
// getEngine is called by ipnserver to get the engine. It's
// not called concurrently and is not called again once it
// successfully returns an engine.
getEngine := func() (wgengine.Engine, error) {
getEngine := func() (wgengine.Engine, *netstack.Impl, error) {
if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
return nil, nil, fmt.Errorf("pretending to be a service failure: %v", msg)
}
for {
res := <-engErrc
if res.Engine != nil {
return res.Engine, nil
return res.Engine, res.Netstack, nil
}
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
// Ignore errors during early boot. Windows 10 auto logs in the GUI
@@ -336,12 +363,12 @@ func startIPNServer(ctx context.Context, logid string) error {
}
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
return nil, nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
}
}
store, err := store.New(logf, statePathOrDefault())
if err != nil {
return err
return fmt.Errorf("store: %w", err)
}
ln, _, err := safesocket.Listen(args.socketpath, safesocket.WindowsLocalPort)

View File

@@ -2,177 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
//go:build ignore
// +build ignore
// The tsshd binary is an SSH server that accepts connections
// The tsshd binary was an experimental SSH server that accepts connections
// from anybody on the same Tailscale network.
//
// It does not use passwords or SSH public key.
// Its functionality moved into tailscaled.
//
// Any user name is accepted; users are logged in as whoever is
// running this daemon.
//
// Warning: use at your own risk. This code has had very few eyeballs
// on it.
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"syscall"
"time"
"unsafe"
"github.com/creack/pty"
"github.com/tailscale/ssh"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
)
var (
port = flag.Int("port", 2200, "port to listen on")
hostKey = flag.String("hostkey", "", "SSH host key")
)
func main() {
flag.Parse()
if *hostKey == "" {
log.Fatalf("missing required --hostkey")
}
hostKey, err := ioutil.ReadFile(*hostKey)
if err != nil {
log.Fatal(err)
}
signer, err := gossh.ParsePrivateKey(hostKey)
if err != nil {
log.Printf("failed to parse SSH host key: %v", err)
return
}
warned := false
for {
addrs, iface, err := interfaces.Tailscale()
if err != nil {
log.Fatalf("listing interfaces: %v", err)
}
if len(addrs) == 0 {
if !warned {
log.Printf("no tailscale interface found; polling until one is available")
warned = true
}
// TODO: use netlink or other OS-specific mechanism to efficiently
// wait for change in interfaces. Polling every N seconds is good enough
// for now.
time.Sleep(5 * time.Second)
continue
}
warned = false
var addr netaddr.IP
for _, a := range addrs {
if a.Is4() {
addr = a
break
}
}
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
s := &ssh.Server{
Addr: listen,
Handler: handleSSH,
}
s.AddHostKey(signer)
err = s.ListenAndServe()
log.Fatalf("tailscale sshd failed: %v", err)
}
}
func handleSSH(s ssh.Session) {
user := s.User()
addr := s.RemoteAddr()
ta, ok := addr.(*net.TCPAddr)
if !ok {
log.Printf("tsshd: rejecting non-TCP addr %T %v", addr, addr)
s.Exit(1)
return
}
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
if !ok {
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
s.Exit(1)
return
}
if !tsaddr.IsTailscaleIP(tanetaddr) {
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return
}
log.Printf("new session for %q from %v", user, ta)
defer log.Printf("closing session for %q from %v", user, ta)
ptyReq, winCh, isPty := s.Pty()
if !isPty {
fmt.Fprintf(s, "TODO scp etc")
s.Exit(1)
return
}
userWantsShell := len(s.Command()) == 0
if userWantsShell {
shell, err := shellOfUser(s.User())
if err != nil {
fmt.Fprintf(s, "failed to find shell: %v\n", err)
s.Exit(1)
return
}
cmd := exec.Command(shell)
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
f, err := pty.Start(cmd)
if err != nil {
log.Printf("running shell: %v", err)
s.Exit(1)
return
}
defer f.Close()
go func() {
for win := range winCh {
setWinsize(f, win.Width, win.Height)
}
}()
go func() {
io.Copy(f, s) // stdin
}()
io.Copy(s, f) // stdout
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
} else {
s.Exit(0)
}
return
}
fmt.Fprintf(s, "TODO: args\n")
s.Exit(1)
}
func shellOfUser(user string) (string, error) {
// TODO
return "/bin/bash", nil
}
func setWinsize(f *os.File, w, h int) {
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
}
// See https://github.com/tailscale/tailscale/issues/3802

View File

@@ -1,12 +0,0 @@
// 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.
//go:build windows
// +build windows
package main
func main() {
panic("tsshd does not work on windows yet")
}

59
cmd/viewer/tests/tests.go Normal file
View File

@@ -0,0 +1,59 @@
// 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 tests serves a list of tests for tailscale.com/cmd/viewer.
package tests
import (
"fmt"
"inet.af/netaddr"
)
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
type StructWithoutPtrs struct {
Int int
Pfx netaddr.IPPrefix
}
type Map struct {
Int map[string]int
SliceInt map[string][]int
StructWithPtr map[string]*StructWithPtrs
StructWithoutPtr map[string]*StructWithoutPtrs
SlicesWithPtrs map[string][]*StructWithPtrs
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"`
// Unsupported.
SliceIntPtr map[string][]*int
PointerKey map[*string]int `json:"-"`
StructWithPtrKey map[StructWithPtrs]int `json:"-"`
}
type StructWithPtrs struct {
Value *StructWithoutPtrs
Int *int
NoCloneValue *StructWithoutPtrs `codegen:"noclone"`
}
func (v *StructWithPtrs) String() string { return fmt.Sprintf("%v", v.Int) }
func (v *StructWithPtrs) Equal(v2 *StructWithPtrs) bool {
return v.Value == v2.Value
}
type StructWithSlices struct {
Values []StructWithoutPtrs
ValuePointers []*StructWithoutPtrs
StructPointers []*StructWithPtrs
Structs []StructWithPtrs
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Data []byte
}

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.
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package tests
import (
"inet.af/netaddr"
)
// Clone makes a deep copy of StructWithPtrs.
// The result aliases no memory with the original.
func (src *StructWithPtrs) Clone() *StructWithPtrs {
if src == nil {
return nil
}
dst := new(StructWithPtrs)
*dst = *src
if dst.Value != nil {
dst.Value = new(StructWithoutPtrs)
*dst.Value = *src.Value
}
if dst.Int != nil {
dst.Int = new(int)
*dst.Int = *src.Int
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithPtrsCloneNeedsRegeneration = StructWithPtrs(struct {
Value *StructWithoutPtrs
Int *int
NoCloneValue *StructWithoutPtrs
}{})
// Clone makes a deep copy of StructWithoutPtrs.
// The result aliases no memory with the original.
func (src *StructWithoutPtrs) Clone() *StructWithoutPtrs {
if src == nil {
return nil
}
dst := new(StructWithoutPtrs)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithoutPtrsCloneNeedsRegeneration = StructWithoutPtrs(struct {
Int int
Pfx netaddr.IPPrefix
}{})
// Clone makes a deep copy of Map.
// The result aliases no memory with the original.
func (src *Map) Clone() *Map {
if src == nil {
return nil
}
dst := new(Map)
*dst = *src
if dst.Int != nil {
dst.Int = map[string]int{}
for k, v := range src.Int {
dst.Int[k] = v
}
}
if dst.SliceInt != nil {
dst.SliceInt = map[string][]int{}
for k := range src.SliceInt {
dst.SliceInt[k] = append([]int{}, src.SliceInt[k]...)
}
}
if dst.StructWithPtr != nil {
dst.StructWithPtr = map[string]*StructWithPtrs{}
for k, v := range src.StructWithPtr {
dst.StructWithPtr[k] = v.Clone()
}
}
if dst.StructWithoutPtr != nil {
dst.StructWithoutPtr = map[string]*StructWithoutPtrs{}
for k, v := range src.StructWithoutPtr {
dst.StructWithoutPtr[k] = v.Clone()
}
}
if dst.SlicesWithPtrs != nil {
dst.SlicesWithPtrs = map[string][]*StructWithPtrs{}
for k := range src.SlicesWithPtrs {
dst.SlicesWithPtrs[k] = append([]*StructWithPtrs{}, src.SlicesWithPtrs[k]...)
}
}
if dst.SlicesWithoutPtrs != nil {
dst.SlicesWithoutPtrs = map[string][]*StructWithoutPtrs{}
for k := range src.SlicesWithoutPtrs {
dst.SlicesWithoutPtrs[k] = append([]*StructWithoutPtrs{}, src.SlicesWithoutPtrs[k]...)
}
}
if dst.StructWithoutPtrKey != nil {
dst.StructWithoutPtrKey = map[StructWithoutPtrs]int{}
for k, v := range src.StructWithoutPtrKey {
dst.StructWithoutPtrKey[k] = v
}
}
if dst.SliceIntPtr != nil {
dst.SliceIntPtr = map[string][]*int{}
for k := range src.SliceIntPtr {
dst.SliceIntPtr[k] = append([]*int{}, src.SliceIntPtr[k]...)
}
}
if dst.PointerKey != nil {
dst.PointerKey = map[*string]int{}
for k, v := range src.PointerKey {
dst.PointerKey[k] = v
}
}
if dst.StructWithPtrKey != nil {
dst.StructWithPtrKey = map[StructWithPtrs]int{}
for k, v := range src.StructWithPtrKey {
dst.StructWithPtrKey[k] = v
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _MapCloneNeedsRegeneration = Map(struct {
Int map[string]int
SliceInt map[string][]int
StructWithPtr map[string]*StructWithPtrs
StructWithoutPtr map[string]*StructWithoutPtrs
SlicesWithPtrs map[string][]*StructWithPtrs
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int
SliceIntPtr map[string][]*int
PointerKey map[*string]int
StructWithPtrKey map[StructWithPtrs]int
}{})
// Clone makes a deep copy of StructWithSlices.
// The result aliases no memory with the original.
func (src *StructWithSlices) Clone() *StructWithSlices {
if src == nil {
return nil
}
dst := new(StructWithSlices)
*dst = *src
dst.Values = append(src.Values[:0:0], src.Values...)
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
for i := range dst.ValuePointers {
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
}
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
for i := range dst.StructPointers {
dst.StructPointers[i] = src.StructPointers[i].Clone()
}
dst.Structs = make([]StructWithPtrs, len(src.Structs))
for i := range dst.Structs {
dst.Structs[i] = *src.Structs[i].Clone()
}
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
x := *src.Ints[i]
dst.Ints[i] = &x
}
dst.Slice = append(src.Slice[:0:0], src.Slice...)
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)
dst.Data = append(src.Data[:0:0], src.Data...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
Values []StructWithoutPtrs
ValuePointers []*StructWithoutPtrs
StructPointers []*StructWithPtrs
Structs []StructWithPtrs
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Data []byte
}{})

View File

@@ -0,0 +1,316 @@
// 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.
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package tests
import (
"encoding/json"
"errors"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
// View returns a readonly view of StructWithPtrs.
func (p *StructWithPtrs) View() StructWithPtrsView {
return StructWithPtrsView{ж: p}
}
// StructWithPtrsView provides a read-only view over StructWithPtrs.
//
// Its methods should only be called if `Valid()` returns true.
type StructWithPtrsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructWithPtrs
}
// Valid reports whether underlying value is non-nil.
func (v StructWithPtrsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v StructWithPtrsView) AsStruct() *StructWithPtrs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v StructWithPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *StructWithPtrsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x StructWithPtrs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v StructWithPtrsView) Value() *StructWithoutPtrs {
if v.ж.Value == nil {
return nil
}
x := *v.ж.Value
return &x
}
func (v StructWithPtrsView) Int() *int {
if v.ж.Int == nil {
return nil
}
x := *v.ж.Int
return &x
}
func (v StructWithPtrsView) NoCloneValue() *StructWithoutPtrs { return v.ж.NoCloneValue }
func (v StructWithPtrsView) String() string { return v.ж.String() }
func (v StructWithPtrsView) Equal(v2 StructWithPtrsView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithPtrsViewNeedsRegeneration = StructWithPtrs(struct {
Value *StructWithoutPtrs
Int *int
NoCloneValue *StructWithoutPtrs
}{})
// View returns a readonly view of StructWithoutPtrs.
func (p *StructWithoutPtrs) View() StructWithoutPtrsView {
return StructWithoutPtrsView{ж: p}
}
// StructWithoutPtrsView provides a read-only view over StructWithoutPtrs.
//
// Its methods should only be called if `Valid()` returns true.
type StructWithoutPtrsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructWithoutPtrs
}
// Valid reports whether underlying value is non-nil.
func (v StructWithoutPtrsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v StructWithoutPtrsView) AsStruct() *StructWithoutPtrs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v StructWithoutPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *StructWithoutPtrsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x StructWithoutPtrs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v StructWithoutPtrsView) Int() int { return v.ж.Int }
func (v StructWithoutPtrsView) Pfx() netaddr.IPPrefix { return v.ж.Pfx }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithoutPtrsViewNeedsRegeneration = StructWithoutPtrs(struct {
Int int
Pfx netaddr.IPPrefix
}{})
// View returns a readonly view of Map.
func (p *Map) View() MapView {
return MapView{ж: p}
}
// MapView provides a read-only view over Map.
//
// Its methods should only be called if `Valid()` returns true.
type MapView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Map
}
// Valid reports whether underlying value is non-nil.
func (v MapView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v MapView) AsStruct() *Map {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v MapView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *MapView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Map
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v MapView) Int() views.Map[string, int] { return views.MapOf(v.ж.Int) }
func (v MapView) SliceInt() views.MapFn[string, []int, views.Slice[int]] {
return views.MapFnOf(v.ж.SliceInt, func(t []int) views.Slice[int] {
return views.SliceOf(t)
})
}
func (v MapView) StructWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] {
return views.MapFnOf(v.ж.StructWithPtr, func(t *StructWithPtrs) StructWithPtrsView {
return t.View()
})
}
func (v MapView) StructWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] {
return views.MapFnOf(v.ж.StructWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView {
return t.View()
})
}
func (v MapView) SlicesWithPtrs() views.MapFn[string, []*StructWithPtrs, views.SliceView[*StructWithPtrs, StructWithPtrsView]] {
return views.MapFnOf(v.ж.SlicesWithPtrs, func(t []*StructWithPtrs) views.SliceView[*StructWithPtrs, StructWithPtrsView] {
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](t)
})
}
func (v MapView) SlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrs, views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView]] {
return views.MapFnOf(v.ж.SlicesWithoutPtrs, func(t []*StructWithoutPtrs) views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] {
return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](t)
})
}
func (v MapView) StructWithoutPtrKey() views.Map[StructWithoutPtrs, int] {
return views.MapOf(v.ж.StructWithoutPtrKey)
}
func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") }
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _MapViewNeedsRegeneration = Map(struct {
Int map[string]int
SliceInt map[string][]int
StructWithPtr map[string]*StructWithPtrs
StructWithoutPtr map[string]*StructWithoutPtrs
SlicesWithPtrs map[string][]*StructWithPtrs
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
StructWithoutPtrKey map[StructWithoutPtrs]int
SliceIntPtr map[string][]*int
PointerKey map[*string]int
StructWithPtrKey map[StructWithPtrs]int
}{})
// View returns a readonly view of StructWithSlices.
func (p *StructWithSlices) View() StructWithSlicesView {
return StructWithSlicesView{ж: p}
}
// StructWithSlicesView provides a read-only view over StructWithSlices.
//
// Its methods should only be called if `Valid()` returns true.
type StructWithSlicesView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructWithSlices
}
// Valid reports whether underlying value is non-nil.
func (v StructWithSlicesView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v StructWithSlicesView) AsStruct() *StructWithSlices {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v StructWithSlicesView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *StructWithSlicesView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x StructWithSlices
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v StructWithSlicesView) Values() views.Slice[StructWithoutPtrs] {
return views.SliceOf(v.ж.Values)
}
func (v StructWithSlicesView) ValuePointers() views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] {
return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](v.ж.ValuePointers)
}
func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs, StructWithPtrsView] {
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](v.ж.StructPointers)
}
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) }
func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice {
return views.IPPrefixSliceOf(v.ж.Prefixes)
}
func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
Values []StructWithoutPtrs
ValuePointers []*StructWithoutPtrs
StructPointers []*StructWithPtrs
Structs []StructWithPtrs
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Data []byte
}{})

378
cmd/viewer/viewer.go Normal file
View File

@@ -0,0 +1,378 @@
// 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.
// Viewer is a tool to automate the creation of "view" wrapper types that
// provide read-only accessor methods to underlying fields.
package main
import (
"bytes"
"flag"
"fmt"
"go/types"
"html/template"
"log"
"os"
"strings"
"tailscale.com/util/codegen"
)
const viewTemplateStr = `{{define "common"}}
// View returns a readonly view of {{.StructName}}.
func (p *{{.StructName}}) View() {{.ViewName}} {
return {{.ViewName}}{ж: p}
}
// {{.ViewName}} provides a read-only view over {{.StructName}}.
//
// Its methods should only be called if ` + "`Valid()`" + ` returns true.
type {{.ViewName}} struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *{{.StructName}}
}
// Valid reports whether underlying value is non-nil.
func (v {{.ViewName}}) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v {{.ViewName}}) AsStruct() *{{.StructName}}{
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v {{.ViewName}}) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x {{.StructName}}
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж=&x
return nil
}
{{end}}
{{define "valueField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
{{end}}
{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) }
{{end}}
{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.{{.FieldName}}) }
{{end}}
{{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
{{end}}
{{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
{{end}}
{{define "viewField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}}View { return v.ж.{{.FieldName}}.View() }
{{end}}
{{define "valuePointerField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {
if v.ж.{{.FieldName}} == nil {
return nil
}
x := *v.ж.{{.FieldName}}
return &x
}
{{end}}
{{define "mapField"}}
func(v {{.ViewName}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
{{end}}
{{define "mapFnField"}}
func(v {{.ViewName}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
return {{.MapFn}}
})}
{{end}}
{{define "unsupportedField"}}func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
{{end}}
{{define "stringFunc"}}func(v {{.ViewName}}) String() string { return v.ж.String() }
{{end}}
{{define "equalFunc"}}func(v {{.ViewName}}) Equal(v2 {{.ViewName}}) bool { return v.ж.Equal(v2.ж) }
{{end}}
`
var viewTemplate *template.Template
func init() {
viewTemplate = template.Must(template.New("view").Parse(viewTemplateStr))
}
func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) {
switch v := t.(type) {
case *types.Pointer:
_, deep, base = requiresCloning(v.Elem())
return true, deep, base
case *types.Slice:
_, deep, base = requiresCloning(v.Elem())
return true, deep, base
}
p := codegen.ContainsPointers(t)
return p, p, t
}
func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thisPkg *types.Package) {
t, ok := typ.Underlying().(*types.Struct)
if !ok || codegen.IsViewType(t) {
return
}
it.Import("encoding/json")
it.Import("errors")
args := struct {
StructName string
ViewName string
FieldName string
FieldType string
FieldViewName string
MapKeyType string
MapValueType string
MapValueView string
MapFn string
}{
StructName: typ.Obj().Name(),
ViewName: typ.Obj().Name() + "View",
}
writeTemplate := func(name string) {
if err := viewTemplate.ExecuteTemplate(buf, name, args); err != nil {
log.Fatal(err)
}
}
writeTemplate("common")
for i := 0; i < t.NumFields(); i++ {
f := t.Field(i)
fname := f.Name()
if !f.Exported() {
continue
}
args.FieldName = fname
fieldType := f.Type()
if codegen.IsInvalid(fieldType) {
continue
}
if !codegen.ContainsPointers(fieldType) || codegen.IsViewType(fieldType) || codegen.HasNoClone(t.Tag(i)) {
args.FieldType = it.QualifiedName(fieldType)
writeTemplate("valueField")
continue
}
switch underlying := fieldType.Underlying().(type) {
case *types.Slice:
slice := underlying
elem := slice.Elem()
args.FieldType = it.QualifiedName(elem)
switch elem.String() {
case "byte":
it.Import("go4.org/mem")
writeTemplate("byteSliceField")
case "inet.af/netaddr.IPPrefix":
it.Import("tailscale.com/types/views")
writeTemplate("ipPrefixSliceField")
default:
it.Import("tailscale.com/types/views")
shallow, deep, base := requiresCloning(elem)
if deep {
if _, isPtr := elem.(*types.Pointer); isPtr {
args.FieldViewName = it.QualifiedName(base) + "View"
writeTemplate("viewSliceField")
} else {
writeTemplate("unsupportedField")
}
continue
} else if shallow {
if _, isBasic := base.(*types.Basic); isBasic {
writeTemplate("unsupportedField")
} else {
args.FieldViewName = it.QualifiedName(base) + "View"
writeTemplate("viewSliceField")
}
continue
}
writeTemplate("sliceField")
}
continue
case *types.Struct, *types.Named:
strucT := underlying
args.FieldType = it.QualifiedName(fieldType)
if codegen.ContainsPointers(strucT) {
writeTemplate("viewField")
continue
}
writeTemplate("valueField")
continue
case *types.Map:
m := underlying
args.FieldType = it.QualifiedName(fieldType)
shallow, deep, key := requiresCloning(m.Key())
if shallow || deep {
writeTemplate("unsupportedField")
continue
}
args.MapKeyType = it.QualifiedName(key)
mElem := m.Elem()
var template string
switch u := mElem.(type) {
case *types.Basic:
template = "mapField"
args.MapValueType = it.QualifiedName(mElem)
case *types.Slice:
slice := u
sElem := slice.Elem()
switch x := sElem.(type) {
case *types.Basic:
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
args.MapValueType = "[]" + sElem.String()
args.MapFn = "views.SliceOf(t)"
template = "mapFnField"
case *types.Pointer:
ptr := x
pElem := ptr.Elem()
switch pElem.(type) {
case *types.Struct, *types.Named:
ptrType := it.QualifiedName(ptr)
viewType := it.QualifiedName(pElem) + "View"
args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
args.MapValueType = "[]" + ptrType
template = "mapFnField"
default:
template = "unsupportedField"
}
default:
template = "unsupportedField"
}
case *types.Pointer:
ptr := u
pElem := ptr.Elem()
switch pElem.(type) {
case *types.Struct, *types.Named:
args.MapValueType = it.QualifiedName(ptr)
args.MapValueView = it.QualifiedName(pElem) + "View"
args.MapFn = "t.View()"
template = "mapFnField"
default:
template = "unsupportedField"
}
default:
template = "unsupportedField"
}
writeTemplate(template)
continue
case *types.Pointer:
ptr := underlying
_, deep, base := requiresCloning(ptr)
if deep {
args.FieldType = it.QualifiedName(base)
writeTemplate("viewField")
} else {
args.FieldType = it.QualifiedName(ptr)
writeTemplate("valuePointerField")
}
continue
}
writeTemplate("unsupportedField")
}
for i := 0; i < typ.NumMethods(); i++ {
f := typ.Method(i)
if !f.Exported() {
continue
}
sig, ok := f.Type().(*types.Signature)
if !ok {
continue
}
switch f.Name() {
case "Clone", "View":
continue // "AsStruct"
case "String":
writeTemplate("stringFunc")
continue
case "Equal":
if sig.Results().Len() == 1 && sig.Results().At(0).Type().String() == "bool" {
writeTemplate("equalFunc")
continue
}
}
}
fmt.Fprintf(buf, "\n")
buf.Write(codegen.AssertStructUnchanged(t, args.StructName, "View", it))
}
var (
flagTypes = flag.String("type", "", "comma-separated list of types; required")
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
)
func main() {
log.SetFlags(0)
log.SetPrefix("viewer: ")
flag.Parse()
if len(*flagTypes) == 0 {
flag.Usage()
os.Exit(2)
}
typeNames := strings.Split(*flagTypes, ",")
var flagArgs []string
flagArgs = append(flagArgs, fmt.Sprintf("-clonefunc=%v", *flagCloneFunc))
if *flagTypes != "" {
flagArgs = append(flagArgs, "-type="+*flagTypes)
}
if *flagBuildTags != "" {
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
}
pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
if err != nil {
log.Fatal(err)
}
it := codegen.NewImportTracker(pkg.Types)
buf := new(bytes.Buffer)
fmt.Fprintf(buf, `//go:generate go run tailscale.com/cmd/cloner %s`, strings.Join(flagArgs, " "))
fmt.Fprintln(buf)
runCloner := false
for _, typeName := range typeNames {
typ, ok := namedTypes[typeName]
if !ok {
log.Fatalf("could not find type %s", typeName)
}
var hasClone bool
for i, n := 0, typ.NumMethods(); i < n; i++ {
if typ.Method(i).Name() == "Clone" {
hasClone = true
break
}
}
if !hasClone {
runCloner = true
}
genView(buf, it, typ, pkg.Types)
}
out := pkg.Name + "_view.go"
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
log.Fatal(err)
}
if runCloner {
// When a new pacakge is added or when existing generated files have
// been deleted, we might run into a case where tailscale.com/cmd/cloner
// has not run yet. We detect this by verifying that all the structs we
// interacted with have had Clone method already generated. If they
// haven't we ask the caller to rerun generation again so that those get
// generated.
log.Printf("%v requires regeneration. Please run go generate again", pkg.Name+"_clone.go")
}
}

View File

@@ -52,10 +52,11 @@ type rxState struct {
sync.Mutex
cipher cipher.AEAD
nonce nonce
buf [maxMessageSize]byte
n int // number of valid bytes in buf
next int // offset of next undecrypted packet
plaintext []byte // slice into buf of decrypted bytes
buf *maxMsgBuffer // or nil when reads exhausted
n int // number of valid bytes in buf
next int // offset of next undecrypted packet
plaintext []byte // slice into buf of decrypted bytes
hdrBuf [headerLen]byte // small buffer used when buf is nil
}
// txState is all the Conn state that Write uses.
@@ -63,7 +64,6 @@ type txState struct {
sync.Mutex
cipher cipher.AEAD
nonce nonce
buf [maxMessageSize]byte
err error // records the first partial write error for all future calls
}
@@ -89,6 +89,10 @@ func (c *Conn) Peer() key.MachinePublic {
// readNLocked reads into c.rx.buf until buf contains at least total
// bytes. Returns a slice of the total bytes in rxBuf, or an
// error if fewer than total bytes are available.
//
// It may be called with a nil c.rx.buf only if total == headerLen.
//
// On success, c.rx.buf will be non-nil.
func (c *Conn) readNLocked(total int) ([]byte, error) {
if total > maxMessageSize {
return nil, errReadTooBig{total}
@@ -97,8 +101,26 @@ func (c *Conn) readNLocked(total int) ([]byte, error) {
if total <= c.rx.n {
return c.rx.buf[:total], nil
}
n, err := c.conn.Read(c.rx.buf[c.rx.n:])
var n int
var err error
if c.rx.buf == nil {
if c.rx.n != 0 || total != headerLen {
panic("unexpected")
}
// Optimization to reduce memory usage.
// Most connections are blocked forever waiting for
// a read, so we don't want c.rx.buf to be allocated until
// we know there's data to read. Instead, when we're
// waiting for data to arrive here, read into the
// 3 byte hdrBuf:
n, err = c.conn.Read(c.rx.hdrBuf[:])
if n > 0 {
c.rx.buf = getMaxMsgBuffer()
copy(c.rx.buf[:], c.rx.hdrBuf[:n])
}
} else {
n, err = c.conn.Read(c.rx.buf[c.rx.n:])
}
c.rx.n += n
if err != nil {
return nil, err
@@ -134,19 +156,19 @@ func (c *Conn) decryptLocked(msg []byte) (err error) {
return err
}
// encryptLocked encrypts plaintext into c.tx.buf (including the
// encryptLocked encrypts plaintext into buf (including the
// packet header) and returns a slice of the ciphertext, or an error
// if the cipher is exhausted (i.e. can no longer be used safely).
func (c *Conn) encryptLocked(plaintext []byte) ([]byte, error) {
func (c *Conn) encryptLocked(plaintext []byte, buf *maxMsgBuffer) ([]byte, error) {
if !c.tx.nonce.Valid() {
// Received 2^64-1 messages on this cipher state. Connection
// is no longer usable.
return nil, errCipherExhausted{}
}
c.tx.buf[0] = msgTypeRecord
binary.BigEndian.PutUint16(c.tx.buf[1:headerLen], uint16(len(plaintext)+chp.Overhead))
ret := c.tx.cipher.Seal(c.tx.buf[:headerLen], c.tx.nonce[:], plaintext, nil)
buf[0] = msgTypeRecord
binary.BigEndian.PutUint16(buf[1:headerLen], uint16(len(plaintext)+chp.Overhead))
ret := c.tx.cipher.Seal(buf[:headerLen], c.tx.nonce[:], plaintext, nil)
c.tx.nonce.Increment()
return ret, nil
@@ -191,6 +213,14 @@ func (c *Conn) decryptOneLocked() error {
c.rx.next = 0
}
// Return our buffer to the pool if it's empty, lest we be
// blocked in a long Read call, reading the 3 byte header. We
// don't to keep that buffer unnecessarily alive.
if c.rx.n == 0 && c.rx.next == 0 && c.rx.buf != nil {
bufPool.Put(c.rx.buf)
c.rx.buf = nil
}
bs, err := c.readNLocked(headerLen)
if err != nil {
return err
@@ -227,6 +257,12 @@ func (c *Conn) Read(bs []byte) (int, error) {
}
n := copy(bs, c.rx.plaintext)
c.rx.plaintext = c.rx.plaintext[n:]
// Lose slice's underlying array pointer to unneeded memory so
// GC can collect more.
if len(c.rx.plaintext) == 0 {
c.rx.plaintext = nil
}
return n, nil
}
@@ -257,6 +293,9 @@ func (c *Conn) Write(bs []byte) (n int, err error) {
return 0, net.ErrClosed
}
buf := getMaxMsgBuffer()
defer bufPool.Put(buf)
var sent int
for len(bs) > 0 {
toSend := bs
@@ -265,7 +304,7 @@ func (c *Conn) Write(bs []byte) (n int, err error) {
}
bs = bs[len(toSend):]
ciphertext, err := c.encryptLocked(toSend)
ciphertext, err := c.encryptLocked(toSend, buf)
if err != nil {
return sent, err
}
@@ -355,3 +394,16 @@ func (n *nonce) Increment() {
}
binary.BigEndian.PutUint64(n[4:], 1+binary.BigEndian.Uint64(n[4:]))
}
type maxMsgBuffer [maxMessageSize]byte
// bufPool holds the temporary buffers for Conn.Read & Write.
var bufPool = &sync.Pool{
New: func() interface{} {
return new(maxMsgBuffer)
},
}
func getMaxMsgBuffer() *maxMsgBuffer {
return bufPool.Get().(*maxMsgBuffer)
}

View File

@@ -13,10 +13,12 @@ import (
"fmt"
"io"
"net"
"runtime"
"strings"
"sync"
"testing"
"testing/iotest"
"time"
chp "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/net/nettest"
@@ -24,6 +26,8 @@ import (
"tailscale.com/types/key"
)
const testProtocolVersion = 1
func TestMessageSize(t *testing.T) {
// This test is a regression guard against someone looking at
// maxCiphertextSize, going "huh, we could be more efficient if it
@@ -205,7 +209,7 @@ func TestConnStd(t *testing.T) {
c2, err = Server(context.Background(), s2, controlKey, nil)
serverErr <- err
}()
c1, err = Client(context.Background(), s1, machineKey, controlKey.Public())
c1, err = Client(context.Background(), s1, machineKey, controlKey.Public(), testProtocolVersion)
if err != nil {
s1.Close()
s2.Close()
@@ -224,6 +228,81 @@ func TestConnStd(t *testing.T) {
})
}
// tests that the idle memory overhead of a Conn blocked in a read is
// reasonable (under 2K). It was previously over 8KB with two 4KB
// buffers for rx/tx. This make sure we don't regress. Hopefully it
// doesn't turn into a flaky test. If so, const max can be adjusted,
// or it can be deleted or reworked.
func TestConnMemoryOverhead(t *testing.T) {
num := 1000
if testing.Short() {
num = 100
}
ng0 := runtime.NumGoroutine()
runtime.GC()
var ms0 runtime.MemStats
runtime.ReadMemStats(&ms0)
var closers []io.Closer
closeAll := func() {
for _, c := range closers {
c.Close()
}
closers = nil
}
defer closeAll()
for i := 0; i < num; i++ {
client, server := pair(t)
closers = append(closers, client, server)
go func() {
var buf [1]byte
client.Read(buf[:])
}()
}
t0 := time.Now()
deadline := t0.Add(3 * time.Second)
var ngo int
for time.Now().Before(deadline) {
runtime.GC()
ngo = runtime.NumGoroutine()
if ngo >= num {
break
}
time.Sleep(10 * time.Millisecond)
}
if ngo < num {
t.Fatalf("only %v goroutines; expected %v+", ngo, num)
}
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
growthTotal := int64(ms.HeapAlloc) - int64(ms0.HeapAlloc)
growthEach := float64(growthTotal) / float64(num)
t.Logf("Alloced %v bytes, %.2f B/each", growthTotal, growthEach)
const max = 2000
if growthEach > max {
t.Errorf("allocated more than expected; want max %v bytes/each", max)
}
closeAll()
// And make sure our goroutines go away too.
deadline = time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
ngo = runtime.NumGoroutine()
if ngo < ng0+num/10 {
break
}
time.Sleep(10 * time.Millisecond)
}
if ngo >= ng0+num/10 {
t.Errorf("goroutines didn't go back down; started at %v, now %v", ng0, ngo)
}
}
// mkConns creates synthetic Noise Conns wrapping the given net.Conns.
// This function is for testing just the Conn transport logic without
// having to muck about with Noise handshakes.
@@ -323,7 +402,7 @@ func pairWithConns(t *testing.T, clientConn, serverConn net.Conn) (*Conn, *Conn)
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, machineKey, controlKey.Public())
client, err := Client(context.Background(), clientConn, machineKey, controlKey.Public(), testProtocolVersion)
if err != nil {
t.Fatalf("client connection failed: %v", err)
}

View File

@@ -32,7 +32,7 @@ const (
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
// protocolVersion is the version of the control protocol that
// Client will use when initiating a handshake.
protocolVersion uint16 = 1
//protocolVersion uint16 = 1
// protocolVersionPrefix is the name portion of the protocol
// name+version string that gets mixed into the handshake as a
// prologue.
@@ -66,7 +66,7 @@ type HandshakeContinuation func(context.Context, net.Conn) (*Conn, error)
// protocol switching. By splitting the handshake into an initial
// message and a continuation, we can embed the handshake initiation
// into the HTTP protocol switching request and avoid a bit of delay.
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
var s symmetricState
s.Initialize()
@@ -78,7 +78,7 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
s.MixHash(controlKey.UntypedBytes())
// -> e, es, s, ss
init := mkInitiationMessage()
init := mkInitiationMessage(protocolVersion)
machineEphemeral := key.NewMachine()
machineEphemeralPub := machineEphemeral.Public()
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
@@ -96,7 +96,7 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
cont := func(ctx context.Context, conn net.Conn) (*Conn, error) {
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey)
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey, protocolVersion)
}
return init[:], cont, nil
}
@@ -107,8 +107,8 @@ func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic)
// This is a helper for when you don't need the fancy
// continuation-style handshake, and just want to synchronously
// upgrade a net.Conn to a secure transport.
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
init, cont, err := ClientDeferred(machineKey, controlKey)
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
init, cont, err := ClientDeferred(machineKey, controlKey, protocolVersion)
if err != nil {
return nil, err
}
@@ -118,7 +118,7 @@ func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, c
return cont(ctx, conn)
}
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic) (*Conn, error) {
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
// No matter what, this function can only run once per s. Ensure
// attempted reuse causes a panic.
defer func() {
@@ -239,9 +239,12 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
} else if _, err := io.ReadFull(conn, init.Header()); err != nil {
return nil, err
}
if init.Version() != protocolVersion {
return nil, sendErr("unsupported protocol version")
}
// Just a rename to make it more obvious what the value is. In the
// current implementation we don't need to block any protocol
// versions at this layer, it's safe to let the handshake proceed
// and then let the caller make decisions based on the agreed-upon
// protocol version.
clientVersion := init.Version()
if init.Type() != msgTypeInitiation {
return nil, sendErr("unexpected handshake message type")
}
@@ -257,7 +260,7 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
// prologue. Can only do this once we at least think the client is
// handshaking using a supported version.
s.MixHash(protocolVersionPrologue(protocolVersion))
s.MixHash(protocolVersionPrologue(clientVersion))
// <- s
// ...
@@ -310,7 +313,7 @@ func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, o
c := &Conn{
conn: conn,
version: protocolVersion,
version: clientVersion,
peer: machineKey,
handshakeHash: s.h,
tx: txState{

View File

@@ -30,7 +30,7 @@ func TestHandshake(t *testing.T) {
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err != nil {
t.Fatalf("client connection failed: %v", err)
}
@@ -42,8 +42,8 @@ func TestHandshake(t *testing.T) {
t.Fatal("client and server disagree on handshake hash")
}
if client.ProtocolVersion() != int(protocolVersion) {
t.Fatalf("client reporting wrong protocol version %d, want %d", client.ProtocolVersion(), protocolVersion)
if client.ProtocolVersion() != int(testProtocolVersion) {
t.Fatalf("client reporting wrong protocol version %d, want %d", client.ProtocolVersion(), testProtocolVersion)
}
if client.ProtocolVersion() != server.ProtocolVersion() {
t.Fatalf("peers disagree on protocol version, client=%d server=%d", client.ProtocolVersion(), server.ProtocolVersion())
@@ -82,7 +82,7 @@ func TestNoReuse(t *testing.T) {
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err != nil {
t.Fatalf("client connection failed: %v", err)
}
@@ -181,7 +181,7 @@ func TestTampering(t *testing.T) {
serverErr <- err
}()
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err == nil {
t.Fatal("client connection succeeded despite tampering")
}
@@ -204,7 +204,7 @@ func TestTampering(t *testing.T) {
serverErr <- err
}()
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
_, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err == nil {
t.Fatal("client connection succeeded despite tampering")
}
@@ -231,7 +231,7 @@ func TestTampering(t *testing.T) {
serverErr <- err
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err != nil {
t.Fatalf("client handshake failed: %v", err)
}
@@ -281,7 +281,7 @@ func TestTampering(t *testing.T) {
}
}()
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public())
client, err := Client(context.Background(), clientConn, clientKey, serverKey.Public(), testProtocolVersion)
if err != nil {
t.Fatalf("client handshake failed: %v", err)
}

View File

@@ -77,7 +77,7 @@ func TestInteropServer(t *testing.T) {
)
go func() {
client, err := Client(context.Background(), s1, machineKey, controlKey.Public())
client, err := Client(context.Background(), s1, machineKey, controlKey.Public(), testProtocolVersion)
clientErr <- err
if err != nil {
return
@@ -121,11 +121,11 @@ func noiseExplorerClient(conn net.Conn, controlKey key.MachinePublic, machineKey
copy(mk.public_key[:], machineKey.Public().UntypedBytes())
var peerKey [32]byte
copy(peerKey[:], controlKey.UntypedBytes())
session := InitSession(true, protocolVersionPrologue(protocolVersion), mk, peerKey)
session := InitSession(true, protocolVersionPrologue(testProtocolVersion), mk, peerKey)
_, msg1 := SendMessage(&session, nil)
var hdr [initiationHeaderLen]byte
binary.BigEndian.PutUint16(hdr[:2], protocolVersion)
binary.BigEndian.PutUint16(hdr[:2], testProtocolVersion)
hdr[2] = msgTypeInitiation
binary.BigEndian.PutUint16(hdr[3:5], 96)
if _, err := conn.Write(hdr[:]); err != nil {
@@ -193,7 +193,7 @@ func noiseExplorerServer(conn net.Conn, controlKey key.MachinePrivate, wantMachi
var mk keypair
copy(mk.private_key[:], controlKey.UntypedBytes())
copy(mk.public_key[:], controlKey.Public().UntypedBytes())
session := InitSession(false, protocolVersionPrologue(protocolVersion), mk, [32]byte{})
session := InitSession(false, protocolVersionPrologue(testProtocolVersion), mk, [32]byte{})
var buf [1024]byte
if _, err := io.ReadFull(conn, buf[:101]); err != nil {

View File

@@ -39,9 +39,9 @@ const (
// 16b: message tag (authenticates the whole message)
type initiationMessage [101]byte
func mkInitiationMessage() initiationMessage {
func mkInitiationMessage(protocolVersion uint16) initiationMessage {
var ret initiationMessage
binary.BigEndian.PutUint16(ret[:2], uint16(protocolVersion))
binary.BigEndian.PutUint16(ret[:2], protocolVersion)
ret[2] = msgTypeInitiation
binary.BigEndian.PutUint16(ret[3:5], uint16(len(ret.Payload())))
return ret

View File

@@ -61,10 +61,9 @@ type Auto struct {
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
hostinfo *tailcfg.Hostinfo
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
inSendStatus int // number of sendStatus calls currently in progress
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
inSendStatus int // number of sendStatus calls currently in progress
state State
authCtx context.Context // context used for auth requests
@@ -290,6 +289,7 @@ func (c *Auto) authRoutine() {
}
if goal == nil {
health.SetAuthRoutineInError(nil)
// Wait for user to Login or Logout.
<-ctx.Done()
c.logf("[v1] authRoutine: context done.")
@@ -297,6 +297,7 @@ func (c *Auto) authRoutine() {
}
if !goal.wantLoggedIn {
health.SetAuthRoutineInError(nil)
err := c.direct.TryLogout(ctx)
goal.sendLogoutError(err)
if err != nil {
@@ -335,6 +336,7 @@ func (c *Auto) authRoutine() {
f = "TryLogin"
}
if err != nil {
health.SetAuthRoutineInError(err)
report(err, f)
bo.BackOff(ctx, err)
continue
@@ -359,6 +361,7 @@ func (c *Auto) authRoutine() {
}
// success
health.SetAuthRoutineInError(nil)
c.mu.Lock()
c.loggedIn = true
c.loginGoal = nil
@@ -555,9 +558,8 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
if !c.direct.SetNetInfo(ni) {
return
}
c.logf("NetInfo: %v", ni)
// Send new Hostinfo (which includes NetInfo) to server
// Send new NetInfo to server
c.sendNewMapRequest()
}
@@ -567,7 +569,6 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
loggedIn := c.loggedIn
synced := c.synced
statusFunc := c.statusFunc
hi := c.hostinfo
c.inSendStatus++
c.mu.Unlock()
@@ -595,7 +596,6 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
URL: url,
Persist: p,
NetMap: nm,
Hostinfo: hi,
State: state,
Err: err,
}

View File

@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestStatusEqual(t *testing.T) {
// Verify that the Equal method stays in sync with reality
equalHandles := []string{"LoginFinished", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
equalHandles := []string{"LoginFinished", "LogoutFinished", "Err", "URL", "NetMap", "State", "Persist"}
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, equalHandles)

View File

@@ -18,7 +18,6 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"strings"
@@ -35,11 +34,13 @@ import (
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/log/logheap"
"tailscale.com/logtail"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
@@ -56,7 +57,8 @@ import (
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
serverURL string // URL of the tailcontrol server
dialer *tsdial.Dialer
serverURL string // URL of the tailcontrol server
timeNow func() time.Time
lastPrintMap time.Time
newDecompressor func() (Decompressor, error)
@@ -69,6 +71,7 @@ type Direct struct {
keepSharerAndUserSplit bool
skipIPForwardingCheck bool
pinger Pinger
popBrowser func(url string) // or nil
mu sync.Mutex // mutex guards the following fields
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
@@ -77,12 +80,12 @@ type Direct struct {
sfGroup singleflight.Group // protects noiseClient creation.
noiseClient *noiseClient
persist persist.Persist
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
persist persist.Persist
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
@@ -100,9 +103,11 @@ type Options struct {
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
DebugFlags []string // debug settings to send to control
LinkMonitor *monitor.Mon // optional link monitor
PopBrowserURL func(url string) // optional func to open browser
Dialer *tsdial.Dialer // non-nil
// KeepSharerAndUserSplit controls whether the client
// understands Node.Sharer. If false, the Sharer is mapped to the User.
@@ -119,11 +124,10 @@ type Options struct {
Pinger Pinger
}
// Pinger is a subset of the wgengine.Engine interface, containing just the Ping method.
// Pinger is the LocalBackend.Ping method.
type Pinger interface {
// Ping is a request to start a discovery or TSMP ping with the peer handling
// the given IP and then call cb with its ping latency & method.
Ping(ip netaddr.IP, useTSMP bool, cb func(*ipnstate.PingResult))
// Ping is a request to do a ping with the peer handling the given IP.
Ping(ctx context.Context, ip netaddr.IP, pingType tailcfg.PingType) (*ipnstate.PingResult, error)
}
type Decompressor interface {
@@ -167,13 +171,12 @@ func NewDirect(opts Options) (*Direct, error) {
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
}
dialer := netns.NewDialer(opts.Logf)
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true
// Disable implicit gzip compression; the various
// handlers (register, map, set-dns, etc) do their own
@@ -198,11 +201,18 @@ func NewDirect(opts Options) (*Direct, error) {
linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
dialer: opts.Dialer,
}
if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New())
} else {
ni := opts.Hostinfo.NetInfo
opts.Hostinfo.NetInfo = nil
c.SetHostinfo(opts.Hostinfo)
if ni != nil {
c.SetNetInfo(ni)
}
}
return c, nil
}
@@ -247,14 +257,11 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.hostinfo == nil {
c.logf("[unexpected] SetNetInfo called with no HostInfo; ignoring NetInfo update: %+v", ni)
if reflect.DeepEqual(ni, c.netinfo) {
return false
}
if reflect.DeepEqual(ni, c.hostinfo.NetInfo) {
return false
}
c.hostinfo.NetInfo = ni.Clone()
c.netinfo = ni.Clone()
c.logf("NetInfo: %v", ni)
return true
}
@@ -331,6 +338,14 @@ type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// hostInfoLocked returns a Clone of c.hostinfo and c.netinfo.
// It must only be called with c.mu held.
func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo {
hi := c.hostinfo.Clone()
hi.NetInfo = c.netinfo.Clone()
return hi
}
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, err error) {
c.mu.Lock()
persist := c.persist
@@ -338,7 +353,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
serverKey := c.serverKey
serverNoiseKey := c.serverNoiseKey
authKey := c.authKey
hi := c.hostinfo.Clone()
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
@@ -372,7 +387,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if err != nil {
return regen, opt.URL, err
}
c.logf("control server key %s from %s", serverKey.ShortString(), c.serverURL)
c.logf("control server key from %s: ts2021=%s, legacy=%v", c.serverURL, keys.PublicKey.ShortString(), keys.LegacyPublicKey.ShortString())
c.mu.Lock()
c.serverKey = keys.LegacyPublicKey
@@ -640,7 +655,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
serverURL := c.serverURL
serverKey := c.serverKey
serverNoiseKey := c.serverNoiseKey
hi := c.hostinfo.Clone()
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
localPort := c.localPort
var epStrs []string
@@ -847,9 +862,17 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {
metricMapResponsePings.Add(1)
go answerPing(c.logf, c.httpc, pr)
go answerPing(c.logf, c.httpc, pr, c.pinger)
}
if u := resp.PopBrowserURL; u != "" && u != sess.lastPopBrowserURL {
sess.lastPopBrowserURL = u
if c.popBrowser != nil {
c.logf("netmap: control says to open URL %v; opening...", u)
c.popBrowser(u)
} else {
c.logf("netmap: control says to open URL %v; no popBrowser func", u)
}
}
if resp.ControlTime != nil && !resp.ControlTime.IsZero() {
c.logf.JSON(1, "controltime", resp.ControlTime.UTC())
}
@@ -858,6 +881,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
} else {
vlogf("netmap: got new map")
}
select {
case timeoutReset <- struct{}{}:
vlogf("netmap: sent timer reset")
@@ -883,6 +907,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
c.logf("exiting process with status %v per controlplane", *code)
os.Exit(*code)
}
if resp.Debug.DisableLogTail {
logtail.Disable()
}
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
@@ -1139,89 +1166,17 @@ func TrimWGConfig() opt.Bool {
//
// It should not return false positives.
//
// TODO(bradfitz): merge this code into LocalBackend.CheckIPForwarding
// and change controlclient.Options.SkipIPForwardingCheck into a
// func([]netaddr.IPPrefix) error signature instead. Then we only have
// one copy of this code.
// TODO(bradfitz): Change controlclient.Options.SkipIPForwardingCheck into a
// func([]netaddr.IPPrefix) error signature instead.
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
if len(routes) == 0 {
// Nothing to route, so no need to warn.
warn, err := netutil.CheckIPForwarding(routes, state)
if err != nil {
// Oh well, we tried. This is just for debugging.
// We don't want false positives.
// TODO: maybe we want a different warning for inability to check?
return false
}
if runtime.GOOS != "linux" {
// We only do subnet routing on Linux for now.
// It might work on darwin/macOS when building from source, so
// don't return true for other OSes. We can OS-based warnings
// already in the admin panel.
return false
}
localIPs := map[netaddr.IP]bool{}
for _, addrs := range state.InterfaceIPs {
for _, pfx := range addrs {
localIPs[pfx.IP()] = true
}
}
v4Routes, v6Routes := false, false
for _, r := range routes {
// It's possible to advertise a route to one of the local
// machine's local IPs. IP forwarding isn't required for this
// to work, so we shouldn't warn for such exports.
if r.IsSingleIP() && localIPs[r.IP()] {
continue
}
if r.IP().Is4() {
v4Routes = true
} else {
v6Routes = true
}
}
if v4Routes {
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil {
// Try another way.
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
}
if err != nil {
// Oh well, we tried. This is just for debugging.
// We don't want false positives.
// TODO: maybe we want a different warning for inability to check?
return false
}
if strings.TrimSpace(string(out)) == "0" {
return true
}
}
if v6Routes {
// Note: you might be wondering why we check only the state of
// conf.all.forwarding, rather than per-interface forwarding
// configuration. According to kernel documentation, it seems
// that to actually forward packets, you need to enable
// forwarding globally, and the per-interface forwarding
// setting only alters other things such as how router
// advertisements are handled. The kernel itself warns that
// enabling forwarding per-interface and not globally will
// probably not work, so I feel okay calling those configs
// broken until we have proof otherwise.
out, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/forwarding")
if err != nil {
out, err = exec.Command("sysctl", "-n", "net.ipv6.conf.all.forwarding").Output()
}
if err != nil {
// Oh well, we tried. This is just for debugging.
// We don't want false positives.
// TODO: maybe we want a different warning for inability to check?
return false
}
if strings.TrimSpace(string(out)) == "0" {
return true
}
}
return false
return warn != nil
}
// isUniquePingRequest reports whether pr contains a new PingRequest.URL
@@ -1241,29 +1196,44 @@ func (c *Direct) isUniquePingRequest(pr *tailcfg.PingRequest) bool {
return true
}
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) {
if pr.URL == "" {
logf("invalid PingRequest with no URL")
return
}
if pr.Types == "" {
answerHeadPing(logf, c, pr)
return
}
for _, t := range strings.Split(pr.Types, ",") {
switch pt := tailcfg.PingType(t); pt {
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP, tailcfg.PingPeerAPI:
go doPingerPing(logf, c, pr, pinger, pt)
default:
logf("unsupported ping request type: %q", t)
}
}
}
func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil)
if err != nil {
logf("http.NewRequestWithContext(%q): %v", pr.URL, err)
logf("answerHeadPing: NewRequestWithContext: %v", err)
return
}
if pr.Log {
logf("answerPing: sending ping to %v ...", pr.URL)
logf("answerHeadPing: sending HEAD ping to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
logf("answerPing error: %v to %v (after %v)", err, pr.URL, d)
logf("answerHeadPing error: %v to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
logf("answerPing complete to %v (after %v)", pr.URL, d)
logf("answerHeadPing complete to %v (after %v)", pr.URL, d)
}
}
@@ -1316,7 +1286,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
return nil, err
}
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL)
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
if err != nil {
return nil, err
}
@@ -1436,35 +1406,35 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
return nc.Do(req)
}
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
// with ping response data.
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
var err error
if pr.URL == "" {
return errors.New("invalid PingRequest with no URL")
}
if pr.IP.IsZero() {
return errors.New("PingRequest without IP")
}
if !strings.Contains(pr.Types, "TSMP") {
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
// doPingerPing sends a Ping to pr.IP using pinger, and sends an http request back to
// pr.URL with ping response data.
func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger, pingType tailcfg.PingType) {
if pr.URL == "" || pr.IP.IsZero() || pinger == nil {
logf("invalid ping request: missing url, ip or pinger")
return
}
start := time.Now()
now := time.Now()
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
// Currently does not check for error since we just return if it fails.
err = postPingResult(now, logf, c, pr, res)
})
return err
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
res, err := pinger.Ping(ctx, pr.IP, pingType)
if err != nil {
d := time.Since(start).Round(time.Millisecond)
logf("doPingerPing: ping error of type %q to %v after %v: %v", pingType, pr.IP, d, err)
return
}
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
}
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
if res.Err != "" {
return errors.New(res.Err)
}
duration := time.Since(now)
func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *tailcfg.PingResponse) error {
duration := time.Since(start)
if pr.Log {
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
if res.Err == "" {
logf("ping to %v completed in %v. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration)
} else {
logf("ping to %v failed after %v: %v", pr.IP, duration, res.Err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
@@ -1474,20 +1444,20 @@ func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg
return err
}
// Send the results of the Ping, back to control URL.
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewReader(jsonPingRes))
if err != nil {
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
}
if pr.Log {
logf("tsmpPing: sending ping results to %v ...", pr.URL)
logf("postPingResult: sending ping results to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
return fmt.Errorf("postPingResult error: %w to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
logf("postPingResult complete to %v (after %v)", pr.URL, d)
}
return nil
}

View File

@@ -14,6 +14,7 @@ import (
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
@@ -30,6 +31,7 @@ func TestNewDirect(t *testing.T) {
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
},
Dialer: new(tsdial.Dialer),
}
c, err := NewDirect(opts)
if err != nil {
@@ -106,6 +108,7 @@ func TestTsmpPing(t *testing.T) {
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
return k, nil
},
Dialer: new(tsdial.Dialer),
}
c, err := NewDirect(opts)
@@ -113,7 +116,8 @@ func TestTsmpPing(t *testing.T) {
t.Fatal(err)
}
pingRes := &ipnstate.PingResult{
pingRes := &tailcfg.PingResponse{
Type: "TSMP",
IP: "123.456.7890",
Err: "",
NodeName: "testnode",

View File

@@ -44,6 +44,7 @@ type mapSession struct {
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string
lastHealth []string
lastPopBrowserURL string
// netMapBuilding is non-nil during a netmapForResponse call,
// containing the value to be returned, once fully populated.

View File

@@ -8,6 +8,7 @@ import (
"context"
"crypto/tls"
"fmt"
"math"
"net"
"net/http"
"net/url"
@@ -17,7 +18,10 @@ import (
"golang.org/x/net/http2"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
)
@@ -43,6 +47,7 @@ func (c *noiseConn) Close() error {
// the ts2021 protocol.
type noiseClient struct {
*http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
privKey key.MachinePrivate
serverPubKey key.MachinePublic
serverHost string // the host:port part of serverURL
@@ -55,7 +60,7 @@ type noiseClient struct {
// newNoiseClient returns a new noiseClient for the provided server and machine key.
// serverURL is of the form https://<host>:<port> (no trailing slash).
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string) (*noiseClient, error) {
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
@@ -72,6 +77,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
serverPubKey: serverPubKey,
privKey: priKey,
serverHost: host,
dialer: dialer,
}
// Create the HTTP/2 Transport using a net/http.Transport
@@ -135,9 +141,6 @@ func (nc *noiseClient) Close() error {
func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
nc.mu.Lock()
connID := nc.nextID
if nc.connPool == nil {
nc.connPool = make(map[int]*noiseConn)
}
nc.nextID++
nc.mu.Unlock()
@@ -146,7 +149,12 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey)
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
// Panic, because a test should have started failing several
// thousand version numbers before getting to this point.
panic("capability version is too high to fit in the wire protocol")
}
conn, err := controlhttp.Dial(ctx, nc.serverHost, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
if err != nil {
return nil, err
}
@@ -154,6 +162,6 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
nc.mu.Lock()
defer nc.mu.Unlock()
ncc := &noiseConn{Conn: conn, id: connID, pool: nc}
nc.connPool[ncc.id] = ncc
mak.Set(&nc.connPool, ncc.id, ncc)
return ncc, 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.
package controlclient
import (
"math"
"testing"
"tailscale.com/tailcfg"
)
// maxAllowedNoiseVersion is the highest we expect the Tailscale
// capability version to ever get. It's a value close to 2^16, but
// with enough leeway that we get a very early warning that it's time
// to rework the wire protocol to allow larger versions, while still
// giving us headroom to bump this test and fix the build.
//
// Code elsewhere in the client will panic() if the tailcfg capability
// version exceeds 16 bits, so take a failure of this test seriously.
const maxAllowedNoiseVersion = math.MaxUint16 - 5000
func TestNoiseVersion(t *testing.T) {
if tailcfg.CurrentCapabilityVersion > maxAllowedNoiseVersion {
t.Fatalf("tailcfg.CurrentCapabilityVersion is %d, want <=%d", tailcfg.CurrentCapabilityVersion, maxAllowedNoiseVersion)
}
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"reflect"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
@@ -75,9 +74,8 @@ type Status struct {
// package, but we have some automated tests elsewhere that need to
// use them. Please don't use these fields.
// TODO(apenwarr): Unexport or remove these.
State State
Persist *persist.Persist // locally persisted configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
State State
Persist *persist.Persist // locally persisted configuration
}
// Equal reports whether s and s2 are equal.
@@ -92,7 +90,6 @@ func (s *Status) Equal(s2 *Status) bool {
s.URL == s2.URL &&
reflect.DeepEqual(s.Persist, s2.Persist) &&
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
s.State == s2.State
}

View File

@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !js
// +build !js
// Package controlhttp implements the Tailscale 2021 control protocol
// base transport over HTTP.
//
@@ -25,36 +28,21 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"time"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/types/key"
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
upgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
handshakeHeaderName = "X-Tailscale-Handshake"
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)
// Dial connects to the HTTP server at addr, requests to switch to the
// Tailscale control protocol, and returns an established control
// protocol connection.
@@ -64,90 +52,158 @@ const (
//
// The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established.
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic) (*controlbase.Conn, error) {
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
a := &dialParams{
ctx: ctx,
host: host,
httpPort: port,
httpsPort: "443",
machineKey: machineKey,
controlKey: controlKey,
version: protocolVersion,
proxyFunc: tshttpproxy.ProxyFromEnvironment,
dialer: dialer,
}
return a.dial()
return a.dial(ctx)
}
type dialParams struct {
ctx context.Context
host string
httpPort string
httpsPort string
machineKey key.MachinePrivate
controlKey key.MachinePublic
version uint16
proxyFunc func(*http.Request) (*url.URL, error) // or nil
dialer dnscache.DialContextFunc
// For tests only
insecureTLS bool
insecureTLS bool
testFallbackDelay time.Duration
}
func (a *dialParams) dial() (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey)
if err != nil {
return nil, err
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
// starting to try a.httpsPort.
func (a *dialParams) httpsFallbackDelay() time.Duration {
if v := a.testFallbackDelay; v != 0 {
return v
}
return 500 * time.Millisecond
}
u := &url.URL{
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
// Create one shared context used by both port 80 and port 443 dials.
// If port 80 is still in flight when 443 returns, this deferred cancel
// will stop the port 80 dial.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
// we'll speak Noise.
u80 := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(a.host, a.httpPort),
Path: serverUpgradePath,
}
conn, httpErr := a.tryURL(u, init)
if httpErr == nil {
ret, err := cont(a.ctx, conn)
if err != nil {
conn.Close()
return nil, err
}
return ret, nil
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.host, a.httpsPort),
Path: serverUpgradePath,
}
// Connecting over plain HTTP failed, assume it's an HTTP proxy
// being difficult and see if we can get through over HTTPS.
u.Scheme = "https"
u.Host = net.JoinHostPort(a.host, a.httpsPort)
init, cont, err = controlbase.ClientDeferred(a.machineKey, a.controlKey)
type tryURLRes struct {
u *url.URL // input (the URL conn+err are for/from)
conn *controlbase.Conn // result (mutually exclusive with err)
err error
}
ch := make(chan tryURLRes) // must be unbuffered
try := func(u *url.URL) {
cbConn, err := a.dialURL(ctx, u)
select {
case ch <- tryURLRes{u, cbConn, err}:
case <-ctx.Done():
if cbConn != nil {
cbConn.Close()
}
}
}
// Start the plaintext HTTP attempt first.
go try(u80)
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
defer try443Timer.Stop()
var err80, err443 error
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err())
case res := <-ch:
if res.err == nil {
return res.conn, nil
}
switch res.u {
case u80:
// Connecting over plain HTTP failed; assume it's an HTTP proxy
// being difficult and see if we can get through over HTTPS.
err80 = res.err
// Stop the fallback timer and run it immediately. We don't use
// Timer.Reset(0) here because on AfterFuncs, that can run it
// again.
if try443Timer.Stop() {
go try(u443)
} // else we lost the race and it started already which is what we want
case u443:
err443 = res.err
default:
panic("invalid")
}
if err80 != nil && err443 != nil {
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443)
}
}
}
}
// dialURL attempts to connect to the given URL.
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
if err != nil {
return nil, err
}
conn, tlsErr := a.tryURL(u, init)
if tlsErr == nil {
ret, err := cont(a.ctx, conn)
if err != nil {
conn.Close()
return nil, err
}
return ret, nil
netConn, err := a.tryURLUpgrade(ctx, u, init)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", httpErr, tlsErr)
cbConn, err := cont(ctx, netConn)
if err != nil {
netConn.Close()
return nil, err
}
return cbConn, nil
}
func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
//
// Only the provided ctx is used, not a.ctx.
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
dns := &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
}
dialer := netns.NewDialer(log.Printf)
tr := http.DefaultTransport.(*http.Transport).Clone()
defer tr.CloseIdleConnections()
tr.Proxy = a.proxyFunc
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dns)
tr.DialContext = dnscache.Dialer(a.dialer, dns)
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
@@ -156,7 +212,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
tr.TLSClientConfig.InsecureSkipVerify = true
tr.TLSClientConfig.VerifyConnection = nil
}
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dns, tr.TLSClientConfig)
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the
@@ -186,7 +242,7 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
connCh <- info.Conn
},
}
ctx := httptrace.WithClientTrace(a.ctx, &trace)
ctx = httptrace.WithClientTrace(ctx, &trace)
req := &http.Request{
Method: "POST",
URL: u,
@@ -232,22 +288,5 @@ func (a *dialParams) tryURL(u *url.URL, init []byte) (net.Conn, error) {
return nil, errors.New("http Transport did not provide a writable body")
}
return &wrappedConn{switchedConn, rwc}, nil
}
type wrappedConn struct {
net.Conn
rwc io.ReadWriteCloser
}
func (w *wrappedConn) Read(bs []byte) (int, error) {
return w.rwc.Read(bs)
}
func (w *wrappedConn) Write(bs []byte) (int, error) {
return w.rwc.Write(bs)
}
func (w *wrappedConn) Close() error {
return w.rwc.Close()
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
}

View File

@@ -0,0 +1,56 @@
// 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 controlhttp
import (
"context"
"encoding/base64"
"net"
"net/url"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/dnscache"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
// Variant of Dial that tunnels the request over WebScokets, since we cannot do
// bi-directional communication over an HTTP connection when in JS.
func Dial(ctx context.Context, addr string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
if err != nil {
return nil, err
}
host, addr, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
wsURL := &url.URL{
Scheme: "ws",
Host: net.JoinHostPort(host, addr),
Path: serverUpgradePath,
// Can't set HTTP headers on the websocket request, so we have to to send
// the handshake via an HTTP header.
RawQuery: url.Values{
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
}.Encode(),
}
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
Subprotocols: []string{upgradeHeaderValue},
})
if err != nil {
return nil, err
}
netConn := wsconn.New(wsConn)
cbConn, err := cont(ctx, netConn)
if err != nil {
netConn.Close()
return nil, err
}
return cbConn, nil
}

View File

@@ -0,0 +1,20 @@
// 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 controlhttp
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
upgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
handshakeHeaderName = "X-Tailscale-Handshake"
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)

View File

@@ -17,22 +17,36 @@ import (
"strconv"
"sync"
"testing"
"time"
"tailscale.com/control/controlbase"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/types/key"
)
type httpTestParam struct {
name string
proxy proxy
// makeHTTPHangAfterUpgrade makes the HTTP response hang after sending a
// 101 switching protocols.
makeHTTPHangAfterUpgrade bool
}
func TestControlHTTP(t *testing.T) {
tests := []struct {
name string
proxy proxy
}{
tests := []httpTestParam{
// direct connection
{
name: "no_proxy",
proxy: nil,
},
// direct connection but port 80 is MITM'ed and broken
{
name: "port80_broken_mitm",
proxy: nil,
makeHTTPHangAfterUpgrade: true,
},
// SOCKS5
{
name: "socks5",
@@ -96,14 +110,16 @@ func TestControlHTTP(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testControlHTTP(t, test.proxy)
testControlHTTP(t, test)
})
}
}
func testControlHTTP(t *testing.T, proxy proxy) {
func testControlHTTP(t *testing.T, param httpTestParam) {
proxy := param.proxy
client, server := key.NewMachine(), key.NewMachine()
const testProtocolVersion = 1
sch := make(chan serverResult, 1)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := AcceptHTTP(context.Background(), w, r, server)
@@ -131,7 +147,11 @@ func testControlHTTP(t *testing.T, proxy proxy) {
t.Fatalf("HTTPS listen: %v", err)
}
httpServer := &http.Server{Handler: handler}
var httpHandler http.Handler = handler
if param.makeHTTPHangAfterUpgrade {
httpHandler = http.HandlerFunc(brokenMITMHandler)
}
httpServer := &http.Server{Handler: httpHandler}
go httpServer.Serve(httpLn)
defer httpServer.Close()
@@ -142,17 +162,24 @@ func testControlHTTP(t *testing.T, proxy proxy) {
go httpsServer.ServeTLS(httpsLn, "", "")
defer httpsServer.Close()
//ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
//defer cancel()
ctx := context.Background()
const debugTimeout = false
if debugTimeout {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
}
a := dialParams{
ctx: context.Background(), //ctx,
host: "localhost",
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
machineKey: client,
controlKey: server.Public(),
insecureTLS: true,
host: "localhost",
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
machineKey: client,
controlKey: server.Public(),
version: testProtocolVersion,
insecureTLS: true,
dialer: new(tsdial.Dialer).SystemDial,
testFallbackDelay: 50 * time.Millisecond,
}
if proxy != nil {
@@ -171,7 +198,7 @@ func testControlHTTP(t *testing.T, proxy proxy) {
}
}
conn, err := a.dial()
conn, err := a.dial(ctx)
if err != nil {
t.Fatalf("dialing controlhttp: %v", err)
}
@@ -213,6 +240,7 @@ type proxy interface {
type socksProxy struct {
sync.Mutex
closed bool
proxy socks5.Server
ln net.Listener
clientConnAddrs map[string]bool // addrs of the local end of outgoing conns from proxy
@@ -228,7 +256,14 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
}
s.ln = ln
s.clientConnAddrs = map[string]bool{}
s.proxy.Logf = t.Logf
s.proxy.Logf = func(format string, a ...any) {
s.Lock()
defer s.Unlock()
if s.closed {
return
}
t.Logf(format, a...)
}
s.proxy.Dialer = s.dialAndRecord
go s.proxy.Serve(ln)
return fmt.Sprintf("socks5://%s", ln.Addr().String())
@@ -237,6 +272,10 @@ func (s *socksProxy) Start(t *testing.T) (url string) {
func (s *socksProxy) Close() {
s.Lock()
defer s.Unlock()
if s.closed {
return
}
s.closed = true
s.ln.Close()
}
@@ -396,3 +435,11 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
Certificates: []tls.Certificate{cert},
}
}
func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
w.(http.Flusher).Flush()
<-r.Context().Done()
}

View File

@@ -5,15 +5,16 @@
package controlhttp
import (
"bufio"
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
@@ -28,6 +29,9 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
http.Error(w, "missing next protocol", http.StatusBadRequest)
return nil, errors.New("no next protocol in HTTP request")
}
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != upgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
@@ -62,9 +66,7 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
conn.Close()
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
}
if brw.Reader.Buffered() > 0 {
conn = &drainBufConn{conn, brw.Reader}
}
conn = netutil.NewDrainBufConn(conn, brw.Reader)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
@@ -75,21 +77,41 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
return nc, nil
}
// drainBufConn is a net.Conn with an initial bunch of bytes in a
// bufio.Reader. Read drains the bufio.Reader until empty, then passes
// through subsequent reads to the Conn directly.
type drainBufConn struct {
net.Conn
r *bufio.Reader
}
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != upgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
if err := r.ParseForm(); err != nil {
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(handshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
func (b *drainBufConn) Read(bs []byte) (int, error) {
if b.r == nil {
return b.Conn.Read(bs)
conn := wsconn.New(c)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
n, err := b.r.Read(bs)
if b.r.Buffered() == 0 {
b.r = nil
}
return n, err
return nc, nil
}

Some files were not shown because too many files have changed in this diff Show More