Compare commits

...

235 Commits

Author SHA1 Message Date
Mihai Parparita
db9962d63b cmd/tailscale/cli: remove special-casing of JS/WASM
We're no longer compiling the entire CLI for WASM, so we can remove
some wrapper functions and GOOS checks.

Effectively reverts 75de4e9cc2.
5df7ac70d6, eda647cb47
and other changes.

Fixes #5146

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-08-02 11:26:27 -07:00
Brad Fitzpatrick
5d0e3d379c go.mod: bump gvisor
For https://github.com/google/gvisor/pull/7850 to quiet macOS warnings.

Updates #5240

Change-Id: Iaa7abab20485c8ff40e5c9b16013aef4fd297a64
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-08-01 16:48:27 -07:00
Joe Tsai
b905db7a56 cmd/derper: remove support for logtail logging (#5248)
There aren't really any useful logs produced by derper.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-08-01 14:36:55 -07:00
Brad Fitzpatrick
357fd85ecf go.toolchain.rev: bump tailscale.go1.19 commit
for mutex slow path metrics: https://github.com/tailscale/go/pull/33

(a maybe temporary experiment)

Change-Id: Idba1ef866e2e1764728bdd869c25becccc1051b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-31 22:10:31 -07:00
Mihai Parparita
c06758c83b cmd/tsconnect: allow SSH username to be specified
Redoes the UI to be a form, with a username field and a host drop-down.

Fixes #5139

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-29 15:54:06 -07:00
Tom DNetto
47f91dd732 cmd/tailscale{,d}: update depaware
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-29 12:16:32 -07:00
Tom DNetto
023d4e2216 tka,types/key: implement NLPrivate glue for tailnet key authority keys
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-29 12:16:32 -07:00
Tom DNetto
7a74466998 shell.nix: update Go toolchain to 1.19
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-29 11:25:47 -07:00
Tom DNetto
44a9b0170b tka: support processing non-primary forks, scenario-driven tests
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-29 11:18:06 -07:00
Maisem Ali
8fd5d3eaf3 tstest: do not error if the there are fewer goroutines than at start
This fixes test failures like:
```
    resource.go:34: goroutine count: expected 3, got 2
```

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-29 09:53:01 -07:00
Tom DNetto
5e61d52f91 tka: implement API surface for generating updates
Based on the builder pattern.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-29 09:36:32 -07:00
Brad Fitzpatrick
acc3b7f259 go.mod: bump inet.af/wf, tidy
This removes inet.af/netaddr from go.{mod,sum}.

Updates #5162

Change-Id: I7121e9fbb96d036cf188c51f0b53731570252d69
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-28 14:50:50 -07:00
Brad Fitzpatrick
f541e00db2 go.toolchain.rev: bump for VERSION file
Updates #5210

Change-Id: Ib6db8b010a6a9369a3eda8a86a49e538e376aff6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-28 14:29:15 -07:00
Brad Fitzpatrick
eae003e56f ipn/ipnlocal: blend existing host SSH keys + newly-generated types as needed
If the host only has RSA, use its RSA + generate ecdsa + ed25519, etc.

Perhaps fixes https://twitter.com/colek42c/status/1550554439299244032 and
something else that was reported.

Change-Id: I88dc475c8e3d95b6f25288ff7664b8e72655fd16
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-28 11:42:58 -07:00
Brad Fitzpatrick
e5176f572e go.toolchain.rev: switch to Go 1.19rc2+
Switch to Go 1.19rc2 in prep for the Go 1.19 GA release on Tuesday.

(We won't be using any Go 1.19 features until then.)

Updates #5210

Change-Id: I94fa0ae8f5645fb7579429668f3970c18d1796d8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-28 11:28:21 -07:00
Brad Fitzpatrick
48e73e147a logtail,logpolicy: tweak minor cosmetic things
Just reading the code again in prep for some alloc reductions.

Change-Id: I065226ea794b7ec7144c2b15942d35131c9313a8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-27 21:13:46 -07:00
Mihai Parparita
ab60f28227 cmd/tsconnect: fix xterm CSS not being imported
@import rules need to come first, they are (silently) ignored otherwise.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-27 16:16:13 -07:00
Mihai Parparita
7c3f480767 cmd/tsconnect: lint during build step
Ensures that TypeScript checks pass before we deploy.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-27 16:12:22 -07:00
James Tucker
d5fb852718 build_dist.sh: add --box and --extra-small flag to produce smaller and boxed binaries
- `--box` when ./cmd/tailscaled is built with this flag, it builds a
  "toybox" style binary that includes tailscale and tailscaled.
- `--extra-small` strip the output binary and omit some dependencies
  (currently AWS integration).

Signed-off-by: James Tucker <james@tailscale.com>
2022-07-27 16:08:52 -07:00
Mihai Parparita
a3d74c4548 cmd/tsconnect: add basic panic handling
The go wasm process exiting is a sign of an unhandled panic. Also
add a explicit recover() call in the notify callback, that's where most
logic bugs are likely to happen (and they may not be fatal).

Also fixes the one panic that was encountered (nill pointer dereference
when generating the JS view of the netmap).

Fixes #5132

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-27 15:42:58 -07:00
Aaron Klotz
4dbdb19c26 net/tshttpproxy: fix incorrect type in Windows implementation, switch to mkwinsyscall, fix memory leak
The definition of winHTTPProxyInfo was using the wrong type (uint16 vs uint32)
for its first field. I fixed that type.

Furthermore, any UTF16 strings returned in that structure must be explicitly
freed. I added code to do this.

Finally, since this is the second time I've seen type safety errors in this code,
I switched the native API calls over to use wrappers generated by mkwinsyscall.
I know that would not have helped prevent the previous two problems, but every
bit helps IMHO.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-07-27 16:33:57 -06:00
Aaron Klotz
446d03e108 scripts: update check_license_headers.sh to skip zsyscall_windows.go
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-07-27 16:19:39 -06:00
Brad Fitzpatrick
97b8c4fa1b ipn/store/awsstore: add "ts_omit_aws" build tag to reduce binary size
Drops tailscaled from 23M to 21M.

Change-Id: I731c542d03113ac94abb695e3c8fcacbc5542712
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-27 14:19:33 -07:00
nyghtowl
e6e1976c3a net/dns: remove systemd-resolved ping
Ping only needed to ensure system awak otherwise utilizing resolvConf to set dns mode.

Signed-off-by: nyghtowl <warrick@tailscale.com>
2022-07-27 14:15:22 -07:00
Mihai Parparita
617a2ec7cc cmd/tsconnect: add Tailwind CSS support and switch to using it
Integrates Tailwind CSS as an esbuild plugin that invokes the CLI
to process the input. It takes ~400ms, so it seems like the easiest
option (vs running a separate process for dev mode).

Existing minimal look and feel is replicated with Tailwind classes,
mostly to prove that the entire system works, including unused
class removal.

Also fixes yarn warnings about package.json not having a license
(which were showing up when invoking any scripts).

Fixes #5136
Fixes #5129

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-27 14:05:45 -07:00
Mihai Parparita
389629258b cmd/tsconnect: switch to TypeScript
Continues to use esbuild for development mode and building. Also
includes a `yarn lint` script that uses tsc to do full type checking.

Fixes #5138

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-27 13:50:34 -07:00
Brad Fitzpatrick
0a6aa75a2d cmd/tailscaled: add opt-in support for linking CLI into daemon
Doesn't help much, though.

    $ go install --tags=ts_include_cli ./cmd/tailscaled/
    $ ls -lh ~/go/bin/tailscaled
    -rwxr-xr-x 2 bradfitz bradfitz 34M Jul 27 11:00 /home/bradfitz/go/bin/tailscaled
    $ go install --tags= ./cmd/tailscaled/
    $ ls -lh ~/go/bin/tailscaled
    -rwxr-xr-x 1 bradfitz bradfitz 23M Jul 27 11:00 /home/bradfitz/go/bin/tailscaled
    $ ls -lh ~/go/bin/tailscale
    -rwxr-xr-x 1 bradfitz bradfitz 13M Jul 25 21:30 /home/bradfitz/go/bin/tailscale

Fixes #2233

Change-Id: I46bae91bb38eb47a76251c1b5c1e9e455fc234b6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-27 11:15:52 -07:00
Brad Fitzpatrick
04cf46a762 util/deephash: fix unexported time.Time hashing
Updates tailscale/corp#6311

Change-Id: I33cd7e4040966261c2f2eb3d32f29936aeb7f632
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-27 09:28:23 -07:00
Maisem Ali
51c3d74095 types/views: add BenchmarkSliceIteration
```
goos: darwin
goarch: arm64
pkg: tailscale.com/types/views
BenchmarkSliceIteration/Len-10            340093              3212 ns/op               0 B/op          0 allocs/op
BenchmarkSliceIteration/Cached-Len-10     366727              3211 ns/op               0 B/op          0 allocs/op
BenchmarkSliceIteration/direct-10         361561              3290 ns/op               0 B/op          0 allocs/op
PASS
ok      tailscale.com/types/views       3.662s
```

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-26 21:25:26 -07:00
Maisem Ali
fa2fbaf3aa tailcfg: add views for SSHRule and SSHPrincipal
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-26 20:57:33 -07:00
Brad Fitzpatrick
7c671b0220 .github/workflows: add gofmt (goimports) check
Change-Id: Iceb3182827b9c65f28f0351e0e254abe4a95e4de
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-26 09:46:06 -07:00
Brad Fitzpatrick
dd3e91b678 go.mod: tidy, remove inet.af/netaddr
It was actually unused earlier, but I had a test program
in my git workdir, keeping go mod tidy from cleaning it.
(more CI needed, perhaps)

Updates #5162

Change-Id: I9047a9aaa6fde7736d6ef516dc3bb652d06fe921
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-25 22:08:20 -07:00
Brad Fitzpatrick
a12aad6b47 all: convert more code to use net/netip directly
perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
    perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
    goimports -w .

Then delete some stuff from the net/netaddr shim package which is no
longer neeed.

Updates #5162

Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-25 21:53:49 -07:00
Brad Fitzpatrick
6a396731eb all: use various net/netip parse funcs directly
Mechanical change with perl+goimports.

Changed {Must,}Parse{IP,IPPrefix,IPPort} to their netip variants, then
goimports -d .

Finally, removed the net/netaddr wrappers, to prevent future use.

Updates #5162

Change-Id: I59c0e38b5fbca5a935d701645789cddf3d7863ad
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-25 21:12:28 -07:00
Brad Fitzpatrick
730ca4203c cmd/tsshd: add a package line to appease gofmt
Change-Id: I2fbbe983186169ddf1995d2f51c7b5a6164a0904
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-25 20:52:04 -07:00
Mihai Parparita
7e4883b261 .github/workflows: add tsconnect static build to wasm GitHub action
Technically not the same as the wasm cross-compilation, but it's
closely connected to it.

Also includes some fixes to tool/yasm to make it actually work on
non-ARM platforms.

Fixes #5134

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-25 18:02:22 -07:00
Brad Fitzpatrick
7eaf5e509f net/netaddr: start migrating to net/netip via new netaddr adapter package
Updates #5162

Change-Id: Id7bdec303b25471f69d542f8ce43805328d56c12
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-25 16:20:43 -07:00
James Tucker
7b1a91dfd3 tool/go: accept a marker file with no line terminator
Somewhere my local configuration or program versions are producing
marker files earlier in the process that lack a line terminator. This
doesn't need to cause an exit via set -e, we can just continue the
process. $extracted matches $REV anyway, so the process works.

Signed-off-by: James Tucker <james@tailscale.com>
2022-07-25 16:06:52 -07:00
Walter Poupore
df9f3edea3 docs/k8s: add prefix to (#5167)
Signed-off-by: Walter Poupore <walterp@tailscale.com>
2022-07-25 15:10:07 -07:00
Denton Gentry
7fd03ad4b4 logpolicy: put QNAP logs buffer in /tmp
Ongoing log writing keeps the spinning disks from hibernating.
Extends earlier implementation for Synology to also handle QNAP.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-25 09:45:04 -07:00
Denton Gentry
f85bb60eba ipn/ipnlocal: prevent attempting to run SSH on QNAP for now
tailscaled runs as a non-root user, SSH is not immediately working.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-25 09:45:04 -07:00
Xe Iaso
904723691b cmd/gitops-pusher: things i forgot to push last PR (#5128)
Signed-off-by: Xe <xe@tailscale.com>
2022-07-25 09:09:32 -04:00
Denton Gentry
4dd799ec43 hostinfo: determine QNAP QTS version
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-22 19:43:47 -07:00
Denton Gentry
d17849461c ipn/{ipnserver,ipnlocal}: support incoming Taildrop on QNAP
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-22 19:43:47 -07:00
Aaron Klotz
1cae618b03 net/dns: add Windows group policy notifications to the NRPT rule manager
As discussed in previous PRs, we can register for notifications when group
policies are updated and act accordingly.

This patch changes nrptRuleDatabase to receive notifications that group policy
has changed and automatically move our NRPT rules between the local and
group policy subkeys as needed.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-07-22 14:24:39 -06:00
Melanie Warrick
f17873e0f4 net/dns: handle D-Bus restarts in resolved manager (#5026)
When dbus restarts it can cause the tailscaled to crash because the nil
signal was not handled in resolved.Fixing so the nil signal leads to a
connection reset and tailscaled stays connected to systemd when dbus restarted.

Fixes #4645

Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: nyghtowl <warrick@tailscale.com>

Co-authored-by: James Tucker <james@tailscale.com>
2022-07-22 12:49:18 -07:00
Xe Iaso
898695e312 cmd/gitops-pusher: add etag cache file for the three version problem (#5124)
This allows gitops-pusher to detect external ACL changes. I'm not
sure what to call this problem, so I've been calling it the "three
version problem" in my notes. The basic problem is that at any given
time we only have two versions of the ACL file at any given point:
the version in CONTROL and the one in the git repo. In order to
check if there has been tampering of the ACL files in the admin
panel, we need to have a _third_ version to compare against.

In this case I am not storing the old ACL entirely (though that could
be a reasonable thing to add in the future), but only its sha256sum.
This allows us to detect if the shasum in control matches the shasum
we expect, and if that expectation fails, then we can react
accordingly.

This will require additional configuration in CI, but I'm sure that
can be done.

Signed-off-by: Xe <xe@tailscale.com>
2022-07-22 15:07:38 -04:00
Brad Fitzpatrick
2024008667 types/key: add MachinePrecomputedSharedKey.Open
Follow-up to cfdb862673

Updates tailscale/corp#1709

Change-Id: I7af931a2cb55f9006e1029381663ac21d1794242
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-22 12:05:29 -07:00
Mihai Parparita
be8a0859a9 cmd/tsconnect: pin yarn and node
Adds a tool/yarn helper script that uses specific versions of yarn and
node, downloading them if necessary.

Modeled after tool/go (and the yarn and node Redo scripts from the
corp repo).

Also allows the path to yarn to be overidden (in case the user does not
want to use this script) and always pipes yarn output (to make debugging
and viewing of process easier).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-22 11:36:49 -07:00
Xe Iaso
92357a54ec cmd/gitops-pusher: fix minor bug with ACL tests (#5123)
Signed-off-by: Xe <xe@tailscale.com>
2022-07-22 13:53:42 -04:00
Brad Fitzpatrick
ba91f57ddd ipn/ipnlocal: ignore empty SSH host key files
Change-Id: I332b0d7d01386111d0af4adf98c96c04d3d12fbb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-22 10:52:04 -07:00
Brad Fitzpatrick
227c6b2a53 ipn/ipnlocal: flesh out error on ssh host key parse error
Change-Id: Iedd2d3898befa536181036b9e9dea59bc777a440
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-22 08:09:59 -07:00
Xe Iaso
90ccba6730 cmd/gitops-pusher: port to use ffcli (#5113)
Signed-off-by: Xe <xe@tailscale.com>
2022-07-22 09:39:24 -04:00
Logan Saso
f7a36dfeb1 api.md: added missing quote to POST DNS Searchpaths request body example
Signed-off-by: Logan Saso <logansaso@gmail.com>
2022-07-21 17:44:51 -07:00
Maisem Ali
9514ed33d2 go.mod: bump gvisor.dev/gvisor
Pick up https://github.com/google/gvisor/pull/7787

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-21 16:41:18 -07:00
Maisem Ali
1d33157ab9 docs/k8s: use job control in run.sh
This has the benefit of propagating SIGINT to tailscaled, which in turn
can react to the event and logout in case of an ephemeral node.

Also fix missing run.sh in Dockerfile.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-21 15:43:40 -07:00
Maisem Ali
3e06b9ea7a ssh/tailssh: add "ssh" to conn logs
Fixes #5089

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-21 12:42:31 -07:00
Maisem Ali
480fd6c797 ssh/tailssh: handle not-authenticated-yet connections in matchRule
Also make more fields in conn.info thread safe, there was previously a
data race here.

Fixes #5110

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-21 12:42:31 -07:00
Xe Iaso
41e60dae80 cmd/gitops-pusher: use fmt.Println for errors (#5112)
Signed-off-by: Xe <xe@tailscale.com>
2022-07-21 13:02:14 -04:00
Brad Fitzpatrick
43f3a969ca types/views: add SliceContains, View.ContainsFunc, View.IndexFunc
We were starting to write these elsewhere as little unexported copies
in misc places.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-21 08:28:43 -07:00
Brad Fitzpatrick
d8cb5aae17 tailcfg, control/controlclient: add tailcfg.PeersChangedPatch [capver 33]
This adds a lighter mechanism for endpoint updates from control.

Change-Id: If169c26becb76d683e9877dc48cfb35f90cc5f24
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-20 15:05:56 -07:00
Mihai Parparita
b763a12331 cmd/tsconnect: allow building static resources in a different directory
When using tsconnect as a module in another repo, we cannot write to
the ./dist directory (modules directories are read-only by default -
there is a -modcacherw flag for `go get` but we can't count on it).

We add a -distdir flag that is honored by both the build and serve
commands for where to place output in.

Somewhat tedious because esbuild outputs paths relative to the working
directory, so we need to do some extra munging to make them relative
to the output directory.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-20 10:14:43 -07:00
Mihai Parparita
de2dcda2e0 .github/workflows: update module that we try to build in cross-wasm
We now have the actual module that we need to build, so switch to
building it directly instead of its (expected) dependencies.

Also fix a copy/paste error in a jsdeps comment.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-20 10:13:30 -07:00
Brad Fitzpatrick
b7f1fe7b0d tailcfg: remove old DNS fields
The control plane server doesn't send these to modern clients so we
don't need them in the tree. The server has its own serialization code
to generate legacy MapResponses when needed.

Change-Id: Idd1e5d96ddf9d4306f2da550d20b77f0c252817a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-19 21:23:12 -07:00
Brad Fitzpatrick
9bd3b5b89c types/key: add ControlPrivate.Shared wrapper too
Follow-up to cfdb862673.

Change-Id: Iab610d761f1e6d88e8bcb584d9c02cafe48fc377
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-19 14:49:01 -07:00
Brad Fitzpatrick
cfdb862673 types/key: add naclbox shared key wrapper type + Seal method
So the control plane can stop doing precomputations on each naclbox
message.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-19 14:18:12 -07:00
Mihai Parparita
6f5096fa61 cmd/tsconnect: initial scaffolding for Tailscale Connect browser client
Runs a Tailscale client in the browser (via a WebAssembly build of the
wasm package) and allows SSH access to machines. The wasm package exports
a newIPN function, which returns a simple JS object with methods like
start(), login(), logout() and ssh(). The golang.org/x/crypto/ssh
package is used for the SSH client.

Terminal emulation and QR code renedring is done via NPM packages (xterm
and qrcode respectively), thus we also need a JS toolchain that can
install and bundle them. Yarn is used for installation, and esbuild
handles loading them and bundling for production serving.

Updates #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-19 13:43:08 -07:00
Brad Fitzpatrick
2a22ea3e83 util/deephash: generate type-specific hasher funcs
name                old time/op    new time/op    delta
Hash-8                71.1µs ± 2%    71.5µs ± 1%     ~     (p=0.114 n=9+8)
HashPacketFilter-8    8.39µs ± 1%    4.83µs ± 2%  -42.38%  (p=0.000 n=8+9)
HashMapAcyclic-8      56.2µs ± 1%    56.9µs ± 2%   +1.17%  (p=0.035 n=10+9)
TailcfgNode-8         6.49µs ± 2%    3.54µs ± 1%  -45.37%  (p=0.000 n=9+9)
HashArray-8            729ns ± 2%     566ns ± 3%  -22.30%  (p=0.000 n=10+10)

name                old alloc/op   new alloc/op   delta
Hash-8                 24.0B ± 0%     24.0B ± 0%     ~     (all equal)
HashPacketFilter-8     24.0B ± 0%     24.0B ± 0%     ~     (all equal)
HashMapAcyclic-8       0.00B          0.00B          ~     (all equal)
TailcfgNode-8          0.00B          0.00B          ~     (all equal)
HashArray-8            0.00B          0.00B          ~     (all equal)

name                old allocs/op  new allocs/op  delta
Hash-8                  1.00 ± 0%      1.00 ± 0%     ~     (all equal)
HashPacketFilter-8      1.00 ± 0%      1.00 ± 0%     ~     (all equal)
HashMapAcyclic-8        0.00           0.00          ~     (all equal)
TailcfgNode-8           0.00           0.00          ~     (all equal)
HashArray-8             0.00           0.00          ~     (all equal)

Change-Id: I34c4e786e748fe60280646d40cc63a2adb2ea6fe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-19 11:33:13 -07:00
Maisem Ali
4d0461f721 ipn/ipnlocal: logout ephemeral nodes on shutdown
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-19 11:29:31 -07:00
Tom DNetto
393a229de9 tka: implement synchronization mechanics
This PR implements the synchronization mechanics for TKA: generating a SyncOffer, processing a SyncOffer to find an intersection,
and computing the set of AUMs that should be transmitted to effect convergence.

This is the final PR implementing core mechanics for TKA.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-19 09:58:36 -07:00
Tom DNetto
165c8f898e tka: implement Authority API surface
After this, there should be one final PR to implement the Sync algorithm!

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-19 09:35:29 -07:00
Brad Fitzpatrick
2491fe1afe tailcfg: add missing omitempty annotation to PopBrowserURL
Change-Id: I8e752afd5bf009c17aae1b53650479b37c3232bd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-19 09:25:06 -07:00
David Anderson
c1cb3efbba net/netcheck: test for OS IPv6 support as well as connectivity.
This lets us distinguish "no IPv6 because the device's ISP doesn't
offer IPv6" from "IPv6 is unavailable/disabled in the OS".

Signed-off-by: David Anderson <danderson@tailscale.com>
2022-07-18 18:02:12 -07:00
Charlotte Brandhorst-Satzkorn
4c0feba38e derp: plumb '/derp' request context through (#5083)
This change is required to implement tracing for derp.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2022-07-18 15:43:03 -07:00
Denton Gentry
3c892d106c VERSION.txt: this is v1.29.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-18 10:23:52 -07:00
Maisem Ali
bd4b27753e docs/k8s: set statedir to /tmp when not specified
This makes `tailscale cert` and Taildrop work on k8s and in ephemeral
mode.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-18 10:19:19 -07:00
Brad Fitzpatrick
469c30c33b ipn/localapi: define a cert dir for Synology DSM6
Fixes #4060

Change-Id: I5f145d4f56f6edb14825268e858d419c55918673
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-18 09:51:24 -07:00
Maisem Ali
c6648db333 cmd/tailscale/cli: make cert use localClient
This was seeminlgy missed in 87ba528ae0.

Fixes #5072

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-17 13:22:29 -07:00
Denton Gentry
9fcda1f0a0 cmd/tailscale/cli/web: add QNAP NAS_SID authentication
QTS 5.0 doesn't always pass a qtoken, in some circumstances
it sends a NAS_SID cookie for us to verify instead.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-17 12:21:03 -07:00
Brad Fitzpatrick
0d52674a84 net/tstun: diagnose /dev/net/tun fd leak, give better failure message
Updates #5029

Change-Id: Ibee5e0c9076fe764eb5d856d5ef8b09f4d0e2921
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-16 14:21:56 -07:00
Brad Fitzpatrick
931f18b575 derp: add missing docs on clientInfo.Version
It's not the Tailscale version.

Change-Id: Icfbd5ff36300b2125b19cd2fa6caa22876965317
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-15 12:00:19 -07:00
Tom DNetto
4f1374ec9e tka: implement consensus & state computation internals
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-15 10:44:43 -07:00
Maisem Ali
af412e8874 ssh/tailssh: better handling of signals and exits
We were not handling errors occurred while copying data between the subprocess and the connection.
This makes it so that we pass the appropriate signals when to the process and the connection.

This also fixes mosh.

Updates #4919

Co-authored-by: James Tucker <raggi@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-07-15 09:06:01 -07:00
Xe Iaso
004f0ca3e0 cmd/gitops-pusher: format HuJSON, enabling exact ACL matches (#5061)
Signed-off-by: Xe <xe@tailscale.com>
2022-07-15 11:09:44 -04:00
Xe Iaso
16c85d0dc5 cmd/gitops-pusher: support GitHub Actions error syntax (#5060)
GitHub Actions lets you annotate lines in files as errors[1]. This syntax
will only fire on syntax errors. Future improvements can extend this to
properly annotate ACL tests too.

[1]: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message

Signed-off-by: Xe <xe@tailscale.com>
2022-07-15 10:20:54 -04:00
Brad Fitzpatrick
7fb6781bda go.toolchain.rev: bump for Go 1.18.4 + runtime timer spin
See https://github.com/tailscale/go/pull/32

Updates #4760
Updates #5030?
Updates #4891?

Change-Id: I066aafddc09fade30a5f3fdee23e6bd200eda9fa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-13 20:56:59 -07:00
Tom DNetto
ec4f849079 tka: implement filesystem-based tailchonk implementation
FS implements Chonk, and given the expected load characteristics (frequent use
of AUM() + ChildAUMs(), and infrequent use of Heads() + CommitVerifiedAUMs()), the
implementation avoids scanning the filesystem to service AUM() and ChildAUMs().

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-13 10:07:17 -07:00
Brad Fitzpatrick
505ca2750d cmd/tailscaled: fix Windows "Allow local LAN access" regression
3f686688a6 regressed the Windows beFirewallKillswitch code,
preventing the /firewall subprocess from running.

Fixes tailscale/corp#6063

Change-Id: Ibd105759e5fecfeffc54f587f8ddcd0f1cbc4dca
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-13 08:01:32 -07:00
David Anderson
96afd1db46 jsondb: small package to load/save JSON DBs.
Signed-off-by: David Anderson <danderson@tailscale.com>
2022-07-12 11:56:38 -07:00
Denton Gentry
755396d6fe tsweb: add Float expvar support in varz
We make assertions about stringification of 0.5. IEEE floating point and
all reasonable proprietary floating point can exactly represent 0.5.
We don't make assertions about other floating point values, too brittle
in tests.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-11 21:16:46 -07:00
Tom DNetto
cca25f6107 tka: implement Chonk type & an in-memory implementation
Chonks are responsible for efficient storage of AUMs and other TKA state.

For testing/prototyping I've implemented an in-memory version, but once we
start to use this from tailscaled we'll need a file-based version.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-11 10:38:29 -07:00
Mihai Parparita
e37167b3ef ipn/localapi: add API for uploading client metrics
Clients may have platform-specific metrics they would like uploaded
(e.g. extracted from MetricKit on iOS). Add a new local API endpoint
that allows metrics to be updated by a simple name/value JSON-encoded
struct.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-07-11 09:51:09 -07:00
Xe Iaso
d6b8a18b09 cmd/proxy-to-grafana: use localClient.GetCertificate (#5024)
Closes #5023

Signed-off-by: Xe <xe@tailscale.com>
2022-07-08 14:33:14 -04:00
Xe Iaso
5bb44a4a5c cmd/gitops-pusher: correctly handle ACL tests failing (#5016)
Apparently the API for running ACL tests returns a 200 if the ACL tests
fail. This is weird, but we can handle it.

Signed-off-by: Xe <xe@tailscale.com>
2022-07-08 10:53:50 -04:00
Andrew Dunham
c7993d2b88 net/dns/resolver: add fuzz/unit test for #2533 (#5018)
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-07-08 09:22:50 -04:00
Tom DNetto
3709074e55 tka: implement State and applying AUMs
Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-07 11:25:26 -07:00
Tom DNetto
1cfd96cdc2 tka: implement AUM and Key types
This is the first in a series of PRs implementing the internals for the
Tailnet Key Authority. This PR implements the AUM and Key types, which
are used by pretty much everything else. Future PRs:

 - The State type & related machinery
 - The Tailchonk (storage) type & implementation
 - The Authority type and sync implementation

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-06 12:19:37 -07:00
Denton Gentry
e6572a0f08 install.sh: Add archarm and Raspbian Stretch
Fixes https://github.com/tailscale/tailscale/issues/4959
Fixes https://github.com/tailscale/tailscale/issues/4897

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-06 05:41:06 -07:00
Joe Tsai
fe3426b4c7 logpolicy: fix config initialization bug (#5010)
If ConfigFromFile cannot find the configuration file,
we must not initialize it with NewConfig.
Instead, we need it to fail validation so that it eventually writes
a newly constructed configuration file.
Otherwise, new tailscale instances will never be able store a persistent
log config and start with a new config file upon every bootup.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-07-05 18:27:31 -07:00
Tom DNetto
d6817d0f22 net/dns/resolver: respond with SERVFAIL if all upstreams fail
Fixes #4722

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-07-05 10:22:52 -07:00
Brad Fitzpatrick
c93fd0d22b go.mod: bump wireguard-go to set CLOEXEC on tun/netlink fds
To get:
c31a7b1ab4

Diff:
95b48cdb39..c31a7b1ab4

Fixes #4992

Change-Id: I077586fefcde923a2721712dd54548f97d010f72
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-07-03 17:11:49 -07:00
Craig Rodrigues
9584d8aa7d docs/k8s: Add env vars for tailscaled args
- TS_SOCKS5_SERVER, argument passed to tailscaled --socks5-server
- TS_OUTBOUND_HTTP_PROXY_LISTEN, argument passed to tailscaled -outbound-http-proxy-listen
- TS_TAILSCALED_EXTRA_ARGS extra arguments passed to tailscaled

Fixes #4985

Signed-off-by: Craig Rodrigues <rodrigc@crodrigues.org>
2022-07-01 14:45:51 -07:00
Jake Edgington
ea6e9099b9 fix: typo rename, ROUTES -> TS_ROUTES
Signed-off-by: Jake Edgington <jake.edgington@gmail.com>
2022-06-30 20:23:37 -07:00
Jake Edgington
72b7edbba9 fix: typo rename, KUBE_SECRET -> TS_KUBE_SECRET
Signed-off-by: Jake Edgington <jake.edgington@gmail.com>
2022-06-30 20:23:37 -07:00
Brad Fitzpatrick
6b71568eb7 util/cloudenv: add Azure support & DNS IPs
And rewrite cloud detection to try to do only zero or one metadata
discovery request for all clouds, only doing a first (or second) as
confidence increases. Work remains for Windows, but a start.

And add Cloud to tailcfg.Hostinfo, which helped with testing using
"tailcfg debug hostinfo".

Updates #4983 (Linux only)
Updates #4984

Change-Id: Ib03337089122ce0cb38c34f724ba4b4812bc614e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-30 17:03:46 -07:00
Mihai Parparita
ec649e707f ipn/ipnlocal: prefer to use one CGNAT route on the Mac
Together with 06aa141632 this minimizes
the number of NEPacketTunnelNetworkSettings updates that we have to do,
and thus avoids Chrome interrupting outstanding requests due to
(perceived) network changes.

Updates #3102

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-30 13:18:52 -07:00
Aaron Bieber
3f4fd64311 cmd/tailscale: exit loop when timeout and pingArgs.num are met
Currently if you use '-c' and ping a host that times out, ping will
continue running indefinitely. This change exits the loop with "no
reply" when we time out, hit the value specified by '-c' and do not
have anyPong. If we have anyPong it returns nil.

Signed-off-by: Aaron Bieber <aaron@bolddaemon.com>
2022-06-30 11:52:56 -07:00
Brad Fitzpatrick
aa37aece9c ipn/ipnlocal, net/dns*, util/cloudenv: add AWS DNS support
And remove the GCP special-casing from ipn/ipnlocal; do it only in the
forwarder for *.internal.

Fixes #4980
Fixes #4981

Change-Id: I5c481e96d91f3d51d274a80fbd37c38f16dfa5cb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-29 20:37:44 -07:00
Brad Fitzpatrick
88c2afd1e3 ipn/ipnlocal, net/dns*, util/cloudenv: specialize DNS config on Google Cloud
This does three things:

* If you're on GCP, it adds a *.internal DNS split route to the
  metadata server, so we never break GCP DNS names. This lets people
  have some Tailscale nodes on GCP and some not (e.g. laptops at home)
  without having to add a Tailnet-wide *.internal DNS route.
  If you already have such a route, though, it won't overwrite it.

* If the 100.100.100.100 DNS forwarder has nowhere to forward to,
  it forwards it to the GCP metadata IP, which forwards to 8.8.8.8.
  This means there are never errNoUpstreams ("upstream nameservers not set")
  errors on GCP due to e.g. mangled /etc/resolv.conf (GCP default VMs
  don't have systemd-resolved, so it's likely a DNS supremacy fight)

* makes the DNS fallback mechanism use the GCP metadata IP as a
  fallback before our hosted HTTP-based fallbacks

I created a default GCP VM from their web wizard. It has no
systemd-resolved.

I then made its /etc/resolv.conf be empty and deleted its GCP
hostnames in /etc/hosts.

I then logged in to a tailnet with no global DNS settings.

With this, tailscaled writes /etc/resolv.conf (direct mode, as no
systemd-resolved) and sets it to 100.100.100.100, which then has
regular DNS via the metadata IP and *.internal DNS via the metadata IP
as well. If the tailnet configures explicit DNS servers, those are used
instead, except for *.internal.

This also adds a new util/cloudenv package based on version/distro
where the cloud type is only detected once. We'll likely expand it in
the future for other clouds, doing variants of this change for other
popular cloud environments.

Fixes #4911

RELNOTES=Google Cloud DNS improvements

Change-Id: I19f3c2075983669b2b2c0f29a548da8de373c7cf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-29 17:39:13 -07:00
Mihai Parparita
6f58497647 .github/workflows: downgrade MinGW to work around windows-race failures
Updates #4926

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-29 14:27:57 -07:00
Maisem Ali
88133c361e Docker: add ALPINE.txt to manage alpine versions
The goal here is to

1. make it so that the number doesn't diverge between the various places
   we had it defined
2. not define the number in corp, only in oss

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-29 11:47:09 -07:00
Maisem Ali
cfa484e1a2 Dockerfile: bump alpine to 3.16
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-29 10:29:37 -07:00
Brad Fitzpatrick
ef351ac0dd Dockerfile: fix build
The Dockerfile directions said:

But that failed with:

Step 14/15 : FROM ghcr.io/tailscale/alpine-base:3.14
Head "https://ghcr.io/v2/tailscale/alpine-base/manifests/3.14": denied: denied

So I guess the Dockerfile.base part was undocumented. But it only had
one line anyway, so move it here to avoid the intermediate layer's
published permissions problem entirely.

Also optimize the cachability a bit while here.

Change-Id: I846ad59fe7e88e6126925689fae78bfb80c279f0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-29 08:18:01 -07:00
Mihai Parparita
06aa141632 wgengine/router: avoid unncessary routing configuration changes
The iOS and macOS networking extension API only exposes a single setter
for the entire routing and DNS configuration, and does not appear to
do any kind of diffing or deltas when applying changes. This results
in spurious "network changed" errors in Chrome, even when the
`OneCGNATRoute` flag from df9ce972c7 is
used (because we're setting the same configuration repeatedly).

Since we already keep track of the current routing and DNS configuration
in CallbackRouter, use that to detect if they're actually changing, and
only invoke the platform setter if it's actually necessary.

Updates #3102

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-28 16:59:37 -07:00
Brad Fitzpatrick
3b1f99ded1 ssh/tailssh: fix Tailscale SSH to Linux Arch machines
See https://github.com/tailscale/tailscale/issues/4924#issuecomment-1168201823

Arch uses a different login binary that makes the -h flag set the PAM
service to "remote". So if they don't have that configured, don't pass -h.

Thanks to @eddiezane for debugging!

Updates #4924

Change-Id: I8d33e0afb2dfb99517bcea2f9d5d0c6247519b3c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-28 15:35:51 -07:00
kylecarbs
9280d39678 wgengine/netstack: close ipstack when netstack.Impl is closed
Fixes netstack.Impl leaking goroutines after shutdown.

Signed-off-by: kylecarbs <kyle@carberry.com>
2022-06-28 14:59:29 -07:00
Denton Gentry
d7f452c0a1 net/portmapper: send discovery packet for IGD specifically.
There appear to be devices out there which send only their
first descriptor in response to a discovery packet for
`ssdp:all`, for example the Sagemcom FAST3890V3 only sends
urn:schemas-wifialliance-org:device:WFADevice:1

Send both ssdp:all and a discovery frame for
InternetGatewayDevice specifically.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-28 03:55:17 -07:00
Denton Gentry
09eaba91ad net/portmap: add a test for Sagemcom FAST3890V3.
Updates https://github.com/tailscale/tailscale/issues/3557

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-28 03:55:17 -07:00
Denton Gentry
b5553c6ad2 net/portmap: run go fmt
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-28 03:55:17 -07:00
Denton Gentry
de4c635e54 net/portmapper: make pcpCodeNotAuthorized log more descriptive
If PCP is present but disabled, turning it on might help
get direct connections.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-28 03:55:17 -07:00
Brad Fitzpatrick
3ac8ab1791 tsnet: add Server.AuthKey field
... so callers can provide the AuthKey via mechanisms other than
environment variables which means multiple Servers can't be started
concurrently in the same process without coordination.

Change-Id: I7736ef4f59b7cc29637939e140e990613ce58e0d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-27 21:57:57 -07:00
Jordan Whited
bef6e2831a cmd/tailscale: move call to cli.CleanUpArgs() from main() into cli.Run() (#4954)
Not all distributions build from package main.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2022-06-27 14:56:25 -07:00
Maisem Ali
40503ef07a ssh/tailssh: fix logging typo
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-27 13:23:09 -07:00
soypete
412c4c55e2 cmd/tailscale: make up respect explicitly empty --operator= value
Fixes #3808

Signed-off-by: soypete <miriah@tailscale.com>
2022-06-27 13:59:45 -06:00
Maisem Ali
c434e47f2d ssh/tailssh: always use current time for policy evaluation
Whenever the SSH policy changes we revaluate all open connections to
make sure they still have access. This check was using the wrong
timestamp and would match against expired policies, however this really
isn't a problem today as we don't have policy that would be impacted by
this check. Fixing it for future use.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-27 12:25:04 -07:00
Maisem Ali
a7d2024e35 ssh/tailssh: allow multiple sessions on the same conn
Fixes #4920
Fixes tailscale/corp#5633
Updates #4479

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-06-27 11:54:54 -07:00
mattn
1d04e01d1e use C:\Windows\System32\OpenSSH\ssh.exe (#4933)
cmd/tailscale: make ssh command prefer Windows ssh.exe over PATH

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
2022-06-25 22:26:21 -07:00
Adam Eijdenberg
9294a14a37 ssh/tailssh: limit setgroups to 16 on macOS
Fixes #4938

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
2022-06-25 22:17:53 -07:00
Adam Eijdenberg
7f807fef6c ssh/tailssh: fix /usr/bin/login args on macOS
Fixes #4931

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
2022-06-25 22:17:53 -07:00
Brad Fitzpatrick
35782f891d util/deephash: add canMemHash func + typeInfo property
Currently unused. (breaking up a bigger change)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-25 13:09:30 -07:00
Brad Fitzpatrick
7b9a901489 util/deephash: add packet filter benchmark
(breaking up parts of another change)

This adds a PacketFilter hashing benchmark with an input that both
contains every possible field, but also is somewhat representative in
the shape of what real packet filters contain.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-25 12:55:15 -07:00
James Tucker
c88bd53b1b .github/workflows: windows-race: print gcc version
Signed-off-by: James Tucker <james@tailscale.com>
2022-06-24 15:30:36 -07:00
Maisem Ali
2d65c1a950 client/tailscale: update ACLRow and ACLTest with new ACL fields
Signed-off-by: Maisem Ali <maisem@tailscale.com>
Co-authored-by: Will Norris <will@tailscale.com>
2022-06-24 12:20:08 -07:00
Aaron Klotz
4baf34cf25 net/dns: set appropriate Windows registry values to prevent it from sending DNS changes concerning our interface to AD domain controllers.
We do this unconditionally inside SetDNS such that the values are always set
before we make any other changes to DNS configurations.

It should not be harmful for the settings to remain even when other DNS
settings are cleared out (since they only affect our network interface).

See https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-dns-dynamic-updates-windows-server-2003 for details about the registry value.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-06-24 11:41:33 -06:00
Aaron Klotz
8cdfd12977 net/dns: update Windows split DNS settings to work alongside other NRPT entries set by group policy.
When there are group policy entries for the NRPT that do not belong to Tailscale,
we recognize that we need to add ourselves to group policy and use that registry
key instead of the local one. We also refresh the group policy settings as
necessary to ensure that our changes take effect immediately.

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

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2022-06-23 15:42:22 -06:00
James Tucker
76256d22d8 wgengine/router: windows: set SkipAsSource on IPv6 LL addresses
Link-local addresses on the Tailscale interface are not routable.
Ideally they would be removed, however, a concern exists that the
operating system will attempt to re-add them which would lead to
thrashing.

Setting SkipAsSource attempts to avoid production of packets using the
address as a source in any default behaviors.

Before, in powershell: `ping (hostname)` would ping the link-local
address of the Tailscale interface, and fail.
After: `ping (hostname)` now pings the link-local address on the next
highest priority metric local interface.

Fixes #4647
Signed-off-by: James Tucker <james@tailscale.com>
2022-06-22 15:26:40 -07:00
Brad Fitzpatrick
8c5c87be26 util/deephash: fix collisions between different types
Updates #4883

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-21 22:29:04 -07:00
Tom DNetto
4f6fa3d63a shell.nix: use 1.18 from system nixpkgs
(Now that 22.05 / 22.11 are released and presumably everyone has updated).

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-06-21 15:36:39 -07:00
Xe Iaso
dee95d0894 cmd/gitops-pusher: add new GitOps assistant (#4893)
This is for an upcoming blogpost on how to manage Tailscale ACLs using a
GitOps flow. This tool is intended to be used in CI and will allow users
to have a git repository be the ultimate source of truth for their ACL
file. This enables ACL changes to be proposed, approved and discussed
before they are applied.

Signed-off-by: Xe <xe@tailscale.com>
2022-06-21 16:04:48 -04:00
Sofiane Hamlaoui
1007983159 Adding pkg for Freebsd installation
Signed-off-by: Sofiane Hamlaoui <s0fianehaml40u1@gmail.com>
2022-06-20 11:31:10 -07:00
Sofiane Hamlaoui
51cc0e503b adding --noconfirm to pacman installation
Signed-off-by: Sofiane Hamlaoui <s0fianehaml40u1@gmail.com>
2022-06-20 11:31:10 -07:00
Brad Fitzpatrick
87a4c75fd4 control/controlclient, ipn/ipnlocal: remove Client.SetExpirySooner, fix race
Client.SetExpirySooner isn't part of the state machine. Remove it from
the Client interface.

And fix a use of LocalBackend.cc without acquiring the lock that
guards that field.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-19 22:08:29 -07:00
Brad Fitzpatrick
ef0d740270 control/controlclient: remove Client.SetStatusFunc
It can't change at runtime. Make it an option.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-19 21:54:39 -07:00
Brad Fitzpatrick
70a2797064 control/controlclient, ipn/ipnlocal: remove some Client methods
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-19 18:11:59 -07:00
Brad Fitzpatrick
a1e429f7c3 control/controlclient, types/netmap: remove unused LocalPort field
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-19 17:22:00 -07:00
Brad Fitzpatrick
526b0b6890 control/controlclient: start simplifying netmap fetch APIs
Step 1 of many, cleaning up the direct/auto client & restarting map
requests that leads to all the unnecessary map requests.

Updates tailscale/corp#5761

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-19 15:43:12 -07:00
Brad Fitzpatrick
467eb2eca0 cmd/tailscale/cli, ipn/ipnlocal: give SSH tips when off/unconfigured
Updates #3802

Change-Id: I6b9a3175f68a6daa670f912561f2c2ececc07770
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-17 19:37:59 -07:00
Brad Fitzpatrick
99ed54926b tailcfg: define some Node.Capabilities about SSH, its config
Updates #3802

Change-Id: Icb4ccbc6bd1c6304013bfc553d04007844a5c0bf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-17 12:14:50 -07:00
Brad Fitzpatrick
13d0b8e6a4 control/controlclient, net/dnscache: use typed singleflight fork
Change-Id: I12be4c5a91ae3a812fe88d9b2d15526fdbb5a921
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-17 10:20:16 -07:00
Brad Fitzpatrick
59ed846277 util/singleflight: add fork of singleflight with generics
Forked from golang.org/x/sync/singleflight at
the x/sync repo's commit 67f06af15bc961c363a7260195bcd53487529a21

Updates golang/go#53427

Change-Id: Iec2b47b7777940017bb9b3db9bd7d93ba4a2e394
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-17 10:20:16 -07:00
Brad Fitzpatrick
757ecf7e80 util/deephash: fix map hashing when key & element have the same type
Regression from 09afb8e35b, in which the
same reflect.Value scratch value was being used as the map iterator
copy destination.

Also: make nil and empty maps hash differently, add test.

Fixes #4871

Co-authored-by: Josh Bleecher Snyder <josharian@gmail.com>
Change-Id: I67f42524bc81f694c1b7259d6682200125ea4a66
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-16 22:29:47 -07:00
Brad Fitzpatrick
22c544bca7 go.mod: bump go4.org/unsafe/assume-no-moving-gc for Go 1.19beta1
Otherwise we crash at startup with Go 1.19beta1.

Updates #4872

Change-Id: I371df4146735f7e066efd2edd48c1a305906c13d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-16 20:40:22 -07:00
Brad Fitzpatrick
f31588786f util/deephash: don't track cycles on non-recursive types
name              old time/op    new time/op    delta
Hash-8              67.3µs ±20%    76.5µs ±16%     ~     (p=0.143 n=10+10)
HashMapAcyclic-8    63.0µs ± 2%    56.3µs ± 1%  -10.65%  (p=0.000 n=10+8)
TailcfgNode-8       9.18µs ± 2%    6.52µs ± 3%  -28.96%  (p=0.000 n=9+10)
HashArray-8          732ns ± 3%     709ns ± 1%   -3.21%  (p=0.000 n=10+10)

name              old alloc/op   new alloc/op   delta
Hash-8               24.0B ± 0%     24.0B ± 0%     ~     (all equal)
HashMapAcyclic-8     0.00B          0.00B          ~     (all equal)
TailcfgNode-8        0.00B          0.00B          ~     (all equal)
HashArray-8          0.00B          0.00B          ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-8                1.00 ± 0%      1.00 ± 0%     ~     (all equal)
HashMapAcyclic-8      0.00           0.00          ~     (all equal)
TailcfgNode-8         0.00           0.00          ~     (all equal)
HashArray-8           0.00           0.00          ~     (all equal)

Change-Id: I28642050d837dff66b2db54b2b0e6d272a930be8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-16 18:39:10 -07:00
Brad Fitzpatrick
36ea837736 util/deephash: fix map hashing to actually hash elements
Fixes #4868

Change-Id: I574fd139cb7f7033dd93527344e6aa0e625477c7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-16 11:32:12 -07:00
Brad Fitzpatrick
4005134263 tailcfg: clarify some of the MapRequest variants
Change-Id: Ia09bd69856e372c3a6b64cda7ddb34029b32a11b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-14 13:08:55 -07:00
Joe Tsai
0f05b2c13f tsnet: use tailscaled as prefix for state file names (#4816)
Use the "tailscaled" prefix instead of "tsnet" for state file names:
1. It is consistent with the pre-existing {{Dir}}/tailscaled.state file.
2. It makes the file layout of `tsnet` and `tailscaled` identical,
   so that they are compatible with each other.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-06-09 11:46:43 -07:00
Denton Gentry
1392a93445 socks5: add a simple test
Start up a backend service, put a SOCKS5 server in front
of it, and verify that we can get data from the backend via
SOCKS5.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-09 10:41:06 -07:00
Denton Gentry
7fffddce8e net/portmapper: enable for iOS
In the 1.27 unstable releases we set the min-version to iOS15,
which means we have 50 MBytes of RAM in the Network Extension.
https://tailscale.com/blog/go-linker/

Include the UPnP/NAT-PMP/PCP portmapper support now that there
is memory for it.

Fixes https://github.com/tailscale/tailscale/issues/2495
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-09 10:40:25 -07:00
Mihai Parparita
2990c2b1cf control/controlhttp: use secure WebSockets protocol by default
Forcing the insecure protocol (and perserving the port number) is only
desired for localhost testing, in prod we need to use wss:// to avoid
mixed-content errors.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-08 15:29:21 -07:00
Tom DNetto
32c6823cf5 tsweb: implement interceptor for error page presentation
Updates https://github.com/tailscale/corp/issues/5605

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-06-08 15:21:16 -07:00
Mihai Parparita
b788a5ba7e tailcfg: add HostinfoView version of TailscaleSSHEnabled
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-08 12:08:14 -07:00
Praneet Loke
d8c05fc1b2 Update a Taildrop error message (#4818)
ipn/ipnlocal: update error message on Taildrop send while not connected

Signed-off-by: Praneet <1466314+praneetloke@users.noreply.github.com>
2022-06-08 09:11:50 -07:00
Brad Fitzpatrick
d3643fa151 cmd/tailscale: add 'debug ts2021' Noise connectivity subcommand
Updates #3488

Change-Id: I9272e68f66c4cf36fb98dd1248a74d3817447690
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-06-08 09:07:05 -07:00
Joe Tsai
96f73b3894 logtail: do not panic in PrivateID.PublicID (#4815)
It is not idiomatic for Go code to panic for situations that
can be normal. For example, if a server receives PrivateID
from a client, it is normal for the server to call
PrivateID.PublicID to validate that the PublicID matches.
However, doing so would panic prior to this change.

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2022-06-07 17:25:05 -07:00
Melanie Warrick
3a182d5dd6 ipn/ipnstate: add ExitNodeStatus to share the exit node if it is in use, the IP, ID and whether its online. (#4761)
-
Updates #4619

Signed-off-by: nyghtowl <warrick@tailscale.com>
2022-06-07 12:31:10 -07:00
Mihai Parparita
6246fa32f0 wasm: add test for disallowed JS/WASM dependencies
Ensures that binary size gains like the ones from #4802 and #4813
don't regress.

Updates #3517

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-07 12:19:11 -07:00
Mihai Parparita
c41837842b wasm: drop pprof dependency
We can use the browser tools to profile, pprof adds 200K to the binary size.

Updates #3157

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-07 12:16:16 -07:00
Ross Zurowski
bb67999808 tailcfg: add TailscaleSSHEnabled helper check (#4812)
This commit adds a helper to check if Tailscale SSH is enabled. We're
currently checking the SSH_HostKeys field in a few places, but later
plan to add an explicit bool. This helper makes the check and any future
changes easier.

Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2022-06-07 14:50:08 -04:00
Will Norris
09363064b5 tsnet: use proper log ID
refactor logpolicy config loading to make it easier to reuse from
outside the package.  Within tsnet, setup a basic logtail config.

Signed-off-by: Will Norris <will@tailscale.com>
2022-06-07 10:09:21 -07:00
Mihai Parparita
edc90ebc61 net/wsconn: remove homegrown wrapper for turning a websocket.Conn into a net.Conn
The one from the nhooyr/websocket package seems to work equally well.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-06-07 09:28:56 -07:00
Denton Gentry
cfb5bd0559 VERSION.txt: this is v1.27.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-06 16:11:18 -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
366 changed files with 18107 additions and 4593 deletions

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

@@ -0,0 +1,52 @@
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 client build
env:
GOOS: js
GOARCH: wasm
run: go build ./cmd/tsconnect/wasm
- name: tsconnect static build
# Use our custom Go toolchain, we set build tags (to control binary size)
# that depend on it.
run: ./tool/go run ./cmd/tsconnect build
- 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

@@ -28,6 +28,11 @@ jobs:
- name: Basic build
run: go build ./cmd/...
- name: Build variants
run: |
go install --tags=ts_include_cli ./cmd/tailscaled
go install --tags=ts_omit_aws ./cmd/tailscaled
- name: Get QEMU
run: |
# The qemu in Ubuntu 20.04 (Focal) is too old; we need 5.x something

View File

@@ -21,6 +21,9 @@ jobs:
- name: Check out code
uses: actions/checkout@v3
- name: Run gofmt (goimports)
run: go run golang.org/x/tools/cmd/goimports -d --format-only .
- name: Run go vet
run: go vet ./...

View File

@@ -43,6 +43,17 @@ jobs:
# TODO(raggi): add a go version here.
key: ${{ runner.os }}-go-2-race-${{ hashFiles('**/go.sum') }}
- name: Print toolchain details
run: gcc -v
# There is currently an issue in the race detector in Go on Windows when
# used with a newer version of GCC.
# See https://github.com/tailscale/tailscale/issues/4926.
- name: Downgrade MinGW
shell: bash
run: |
choco install mingw --version 10.2.0 --allow-downgrade
- name: Test with -race flag
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)

1
ALPINE.txt Normal file
View File

@@ -0,0 +1 @@
3.16

View File

@@ -39,6 +39,18 @@ WORKDIR /go/src/tailscale
COPY go.mod go.sum ./
RUN go mod download
# Pre-build some stuff before the following COPY line invalidates the Docker cache.
RUN go install \
github.com/aws/aws-sdk-go-v2/aws \
github.com/aws/aws-sdk-go-v2/config \
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet \
gvisor.dev/gvisor/pkg/tcpip/stack \
golang.org/x/crypto/ssh \
golang.org/x/crypto/acme \
nhooyr.io/websocket \
github.com/mdlayher/netlink \
golang.zx2c4.com/wireguard/device
COPY . .
# see build_docker.sh
@@ -56,5 +68,8 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled
FROM ghcr.io/tailscale/alpine-base:3.14
FROM alpine:3.16
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
COPY --from=build-env /go/src/tailscale/docs/k8s/run.sh /usr/local/bin/

View File

@@ -2,5 +2,5 @@
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
FROM alpine:3.15
FROM alpine:3.16
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -1 +1 @@
1.25.0
1.29.0

18
api.md
View File

@@ -815,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"
]
}
}
}
@@ -839,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" ]
}
}
}
@@ -859,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" ]}}}
}
```
@@ -1114,7 +1120,7 @@ Replaces the list of searchpaths with the list supplied by the user and returns
`searchPaths` - A list of searchpaths in JSON.
```
{
"searchPaths: ["user1.example.com", "user2.example.com"]
"searchPaths": ["user1.example.com", "user2.example.com"]
}
```

View File

@@ -45,4 +45,25 @@ EOF
exit 0
fi
exec ./tool/go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
tags=""
ldflags="-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}"
# build_dist.sh arguments must precede go build arguments.
while [ "$#" -gt 1 ]; do
case "$1" in
--extra-small)
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws"
;;
--box)
shift
tags="${tags:+$tags,}ts_include_cli"
;;
*)
break
;;
esac
done
exec ./tool/go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"

View File

@@ -25,14 +25,14 @@ export PATH=$PWD/tool:$PATH
eval $(./build_dist.sh shellvars)
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.16"
PUSH="${PUSH:-false}"
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

View File

@@ -13,22 +13,30 @@ import (
"encoding/json"
"fmt"
"net/http"
"inet.af/netaddr"
"net/netip"
)
// ACLRow defines a rule that grants access by a set of users or groups to a set of servers and ports.
// ACLRow defines a rule that grants access by a set of users or groups to a set
// of servers and ports.
// Only one of Src/Dst or Users/Ports may be specified.
type ACLRow struct {
Action string `json:"action,omitempty"` // valid values: "accept"
Users []string `json:"users,omitempty"`
Ports []string `json:"ports,omitempty"`
Users []string `json:"users,omitempty"` // old name for src
Ports []string `json:"ports,omitempty"` // old name for dst
Src []string `json:"src,omitempty"`
Dst []string `json:"dst,omitempty"`
}
// ACLTest defines a test for your ACLs to prevent accidental exposure or revoking of access to key servers and ports.
// ACLTest defines a test for your ACLs to prevent accidental exposure or
// revoking of access to key servers and ports. Only one of Src or User may be
// specified, and only one of Allow/Accept may be specified.
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
Src string `json:"src,omitempty"` // source
User string `json:"user,omitempty"` // old name for source
Accept []string `json:"accept,omitempty"` // expected destination ip:port that user can access
Deny []string `json:"deny,omitempty"` // expected destination ip:port that user cannot access
Allow []string `json:"allow,omitempty"` // old name for accept
}
// ACLDetails contains all the details for an ACL.
@@ -345,7 +353,7 @@ func (c *Client) PreviewACLForUser(ctx context.Context, acl ACL, user string) (r
// 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) {
func (c *Client) PreviewACLForIPPort(ctx context.Context, acl ACL, ipport netip.AddrPort) (res *ACLPreview, err error) {
// Format return errors to be descriptive.
defer func() {
if err != nil {

View File

@@ -19,6 +19,7 @@ import (
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"os/exec"
"runtime"
@@ -28,7 +29,6 @@ import (
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -665,7 +665,7 @@ func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn str
// 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) {
func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
v := url.Values{}
v.Set("ip", ip.String())
v.Set("type", string(pingtype))

View File

@@ -13,15 +13,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"inet.af/netaddr"
"net/netip"
)
// 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"`
AdvertisedRoutes []netip.Prefix `json:"advertisedRoutes"`
EnabledRoutes []netip.Prefix `json:"enabledRoutes"`
}
// Routes retrieves the list of subnet routes that have been enabled for a device.
@@ -56,14 +55,14 @@ func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, e
}
type postRoutesParams struct {
Routes []netaddr.IPPrefix `json:"routes"`
Routes []netip.Prefix `json:"routes"`
}
// SetRoutes updates the list of subnets that are enabled for a device.
// Subnets must be parsable by inet.af/netaddr.ParseIPPrefix.
// Subnets must be parsable by net/netip.ParsePrefix.
// 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) {
func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netip.Prefix) (routes *Routes, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("tailscale.SetRoutes: %w", err)

View File

@@ -29,7 +29,6 @@ import (
"tailscale.com/atomicfile"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/logpolicy"
"tailscale.com/metrics"
"tailscale.com/net/stun"
"tailscale.com/tsweb"
@@ -37,16 +36,15 @@ import (
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
@@ -135,7 +133,6 @@ func main() {
flag.Parse()
if *dev {
*logCollection = ""
*addr = ":3340" // above the keys DERP
log.Printf("Running in dev mode.")
tsweb.DevMode = true
@@ -146,12 +143,6 @@ func main() {
log.Fatalf("invalid server address: %v", err)
}
var logPol *logpolicy.Policy
if *logCollection != "" {
logPol = logpolicy.New(*logCollection)
log.SetOutput(logPol.Logtail)
}
cfg := loadConfig()
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"

View File

@@ -13,7 +13,6 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/derp/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
@@ -45,8 +44,8 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return
}
counterWebSocketAccepts.Add(1)
wc := wsconn.New(c)
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(wc, brw, r.RemoteAddr)
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
})
}

1
cmd/gitops-pusher/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
version-cache.json

View File

@@ -0,0 +1,48 @@
# gitops-pusher
This is a small tool to help people achieve a
[GitOps](https://about.gitlab.com/topics/gitops/) workflow with Tailscale ACL
changes. This tool is intended to be used in a CI flow that looks like this:
```yaml
name: Tailscale ACL syncing
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
acls:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go environment
uses: actions/setup-go@v3.2.0
- name: Install gitops-pusher
run: go install tailscale.com/cmd/gitops-pusher@latest
- name: Deploy ACL
if: github.event_name == 'push'
env:
TS_API_KEY: ${{ secrets.TS_API_KEY }}
TS_TAILNET: ${{ secrets.TS_TAILNET }}
run: |
~/go/bin/gitops-pusher --policy-file ./policy.hujson apply
- name: ACL tests
if: github.event_name == 'pull_request'
env:
TS_API_KEY: ${{ secrets.TS_API_KEY }}
TS_TAILNET: ${{ secrets.TS_TAILNET }}
run: |
~/go/bin/gitops-pusher --policy-file ./policy.hujson test
```
Change the value of the `--policy-file` flag to point to the policy file on
disk. Policy files should be in [HuJSON](https://github.com/tailscale/hujson)
format.

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"os"
)
// Cache contains cached information about the last time this tool was run.
//
// This is serialized to a JSON file that should NOT be checked into git.
// It should be managed with either CI cache tools or stored locally somehow. The
// exact mechanism is irrelevant as long as it is consistent.
//
// This allows gitops-pusher to detect external ACL changes. I'm not sure what to
// call this problem, so I've been calling it the "three version problem" in my
// notes. The basic problem is that at any given time we only have two versions
// of the ACL file at any given point. In order to check if there has been
// tampering of the ACL files in the admin panel, we need to have a _third_ version
// to compare against.
//
// In this case I am not storing the old ACL entirely (though that could be a
// reasonable thing to add in the future), but only its sha256sum. This allows
// us to detect if the shasum in control matches the shasum we expect, and if that
// expectation fails, then we can react accordingly.
type Cache struct {
PrevETag string // Stores the previous ETag of the ACL to allow
}
// Save persists the cache to a given file.
func (c *Cache) Save(fname string) error {
os.Remove(fname)
fout, err := os.Create(fname)
if err != nil {
return err
}
defer fout.Close()
return json.NewEncoder(fout).Encode(c)
}
// LoadCache loads the cache from a given file.
func LoadCache(fname string) (*Cache, error) {
var result Cache
fin, err := os.Open(fname)
if err != nil {
return nil, err
}
defer fin.Close()
err = json.NewDecoder(fin).Decode(&result)
if err != nil {
return nil, err
}
return &result, nil
}
// Shuck removes the first and last character of a string, analogous to
// shucking off the husk of an ear of corn.
func Shuck(s string) string {
return s[1 : len(s)-1]
}

View File

@@ -0,0 +1,373 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Command gitops-pusher allows users to use a GitOps flow for managing Tailscale ACLs.
//
// See README.md for more details.
package main
import (
"context"
"crypto/sha256"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"regexp"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/tailscale/hujson"
)
var (
rootFlagSet = flag.NewFlagSet("gitops-pusher", flag.ExitOnError)
policyFname = rootFlagSet.String("policy-file", "./policy.hujson", "filename for policy file")
cacheFname = rootFlagSet.String("cache-file", "./version-cache.json", "filename for the previous known version hash")
timeout = rootFlagSet.Duration("timeout", 5*time.Minute, "timeout for the entire CI run")
githubSyntax = rootFlagSet.Bool("github-syntax", true, "use GitHub Action error syntax (https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message)")
modifiedExternallyFailure = make(chan struct{}, 1)
)
func modifiedExternallyError() {
if *githubSyntax {
fmt.Printf("::error file=%s,line=1,col=1,title=Policy File Modified Externally::The policy file was modified externally in the admin console.\n", *policyFname)
} else {
fmt.Printf("The policy file was modified externally in the admin console.\n")
}
modifiedExternallyFailure <- struct{}{}
}
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
localEtag, err := sumFile(*policyFname)
if err != nil {
return err
}
if cache.PrevETag == "" {
log.Println("no previous etag found, assuming local file is correct and recording that")
cache.PrevETag = localEtag
}
log.Printf("control: %s", controlEtag)
log.Printf("local: %s", localEtag)
log.Printf("cache: %s", cache.PrevETag)
if cache.PrevETag != controlEtag {
modifiedExternallyError()
}
if controlEtag == localEtag {
cache.PrevETag = localEtag
log.Println("no update needed, doing nothing")
return nil
}
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
return err
}
cache.PrevETag = localEtag
return nil
}
}
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
localEtag, err := sumFile(*policyFname)
if err != nil {
return err
}
if cache.PrevETag == "" {
log.Println("no previous etag found, assuming local file is correct and recording that")
cache.PrevETag = localEtag
}
log.Printf("control: %s", controlEtag)
log.Printf("local: %s", localEtag)
log.Printf("cache: %s", cache.PrevETag)
if cache.PrevETag != controlEtag {
modifiedExternallyError()
}
if controlEtag == localEtag {
log.Println("no updates found, doing nothing")
return nil
}
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
return err
}
return nil
}
}
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
localEtag, err := sumFile(*policyFname)
if err != nil {
return err
}
if cache.PrevETag == "" {
log.Println("no previous etag found, assuming local file is correct and recording that")
cache.PrevETag = Shuck(localEtag)
}
log.Printf("control: %s", controlEtag)
log.Printf("local: %s", localEtag)
log.Printf("cache: %s", cache.PrevETag)
return nil
}
}
func main() {
tailnet, ok := os.LookupEnv("TS_TAILNET")
if !ok {
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
}
apiKey, ok := os.LookupEnv("TS_API_KEY")
if !ok {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
}
cache, err := LoadCache(*cacheFname)
if err != nil {
if os.IsNotExist(err) {
cache = &Cache{}
} else {
log.Fatalf("error loading cache: %v", err)
}
}
defer cache.Save(*cacheFname)
applyCmd := &ffcli.Command{
Name: "apply",
ShortUsage: "gitops-pusher [options] apply",
ShortHelp: "Pushes changes to CONTROL",
LongHelp: `Pushes changes to CONTROL`,
Exec: apply(cache, tailnet, apiKey),
}
testCmd := &ffcli.Command{
Name: "test",
ShortUsage: "gitops-pusher [options] test",
ShortHelp: "Tests ACL changes",
LongHelp: "Tests ACL changes",
Exec: test(cache, tailnet, apiKey),
}
cksumCmd := &ffcli.Command{
Name: "checksum",
ShortUsage: "Shows checksums of ACL files",
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
Exec: getChecksums(cache, tailnet, apiKey),
}
root := &ffcli.Command{
ShortUsage: "gitops-pusher [options] <command>",
ShortHelp: "Push Tailscale ACLs to CONTROL using a GitOps workflow",
Subcommands: []*ffcli.Command{applyCmd, cksumCmd, testCmd},
FlagSet: rootFlagSet,
}
if err := root.Parse(os.Args[1:]); err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
if err := root.Run(ctx); err != nil {
fmt.Println(err)
os.Exit(1)
}
if len(modifiedExternallyFailure) != 0 {
os.Exit(1)
}
}
func sumFile(fname string) (string, error) {
data, err := os.ReadFile(fname)
if err != nil {
return "", err
}
formatted, err := hujson.Format(data)
if err != nil {
return "", err
}
h := sha256.New()
_, err = h.Write(formatted)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
fin, err := os.Open(policyFname)
if err != nil {
return err
}
defer fin.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), fin)
if err != nil {
return err
}
req.SetBasicAuth(apiKey, "")
req.Header.Set("Content-Type", "application/hujson")
req.Header.Set("If-Match", `"`+oldEtag+`"`)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
got := resp.StatusCode
want := http.StatusOK
if got != want {
var ate ACLTestError
err := json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
}
return ate
}
return nil
}
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
fin, err := os.Open(policyFname)
if err != nil {
return err
}
defer fin.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), fin)
if err != nil {
return err
}
req.SetBasicAuth(apiKey, "")
req.Header.Set("Content-Type", "application/hujson")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var ate ACLTestError
err = json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
}
if len(ate.Message) != 0 || len(ate.Data) != 0 {
return ate
}
got := resp.StatusCode
want := http.StatusOK
if got != want {
return fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
}
return nil
}
var lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)
type ACLTestError struct {
Message string `json:"message"`
Data []ACLTestErrorDetail `json:"data"`
}
func (ate ACLTestError) Error() string {
var sb strings.Builder
if *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {
sp := lineColMessageSplit.FindStringSubmatch(ate.Message)
line := sp[1]
col := sp[2]
msg := sp[3]
fmt.Fprintf(&sb, "::error file=%s,line=%s,col=%s::%s", *policyFname, line, col, msg)
} else {
fmt.Fprintln(&sb, ate.Message)
}
fmt.Fprintln(&sb)
for _, data := range ate.Data {
fmt.Fprintf(&sb, "For user %s:\n", data.User)
for _, err := range data.Errors {
fmt.Fprintf(&sb, "- %s\n", err)
}
}
return sb.String()
}
type ACLTestErrorDetail struct {
User string `json:"user"`
Errors []string `json:"errors"`
}
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl", tailnet), nil)
if err != nil {
return "", err
}
req.SetBasicAuth(apiKey, "")
req.Header.Set("Accept", "application/hujson")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
got := resp.StatusCode
want := http.StatusOK
if got != want {
return "", fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
}
return Shuck(resp.Header.Get("ETag")), nil
}

View File

@@ -135,13 +135,13 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
return ""
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
return nodeIP.IP().String()
if nodeIP.Addr().Is4() && nodeIP.IsSingleIP() {
return nodeIP.Addr().String()
}
}
for _, nodeIP := range who.Node.Addresses {
if nodeIP.IsSingleIP() {
return nodeIP.IP().String()
return nodeIP.Addr().String()
}
}
return ""

View File

@@ -19,10 +19,15 @@ import (
var (
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
alpine = flag.Bool("alpine", false, "print the tag of alpine docker image")
)
func main() {
flag.Parse()
if *alpine {
fmt.Println(strings.TrimSpace(ts.AlpineDockerTag))
return
}
if *goToolchain {
fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
}

View File

@@ -84,7 +84,7 @@ func main() {
if *useHTTPS {
ln, err = ts.Listen("tcp", ":443")
ln = tls.NewListener(ln, &tls.Config{
GetCertificate: tailscale.GetCertificate,
GetCertificate: localClient.GetCertificate,
})
go func() {

View File

@@ -7,6 +7,7 @@ package cli
import (
"context"
"errors"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
)
@@ -31,6 +32,6 @@ func runBugReport(ctx context.Context, args []string) error {
if err != nil {
return err
}
outln(logMarker)
fmt.Println(logMarker)
return nil
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/atomicfile"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/version"
)
@@ -28,7 +27,7 @@ var certCmd = &ffcli.Command{
ShortHelp: "get TLS certs",
ShortUsage: "cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert")
fs := flag.NewFlagSet("cert", flag.ExitOnError)
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
@@ -46,7 +45,7 @@ func runCert(ctx context.Context, args []string) error {
if certArgs.serve {
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: tailscale.GetCertificate,
GetCertificate: localClient.GetCertificate,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
@@ -80,7 +79,7 @@ func runCert(ctx context.Context, args []string) error {
domain := args[0]
printf := func(format string, a ...any) {
printf(format, a...)
fmt.Printf(format, a...)
}
if certArgs.certFile == "-" || certArgs.keyFile == "-" {
printf = log.Printf
@@ -90,7 +89,7 @@ func runCert(ctx context.Context, args []string) error {
certArgs.certFile = domain + ".crt"
certArgs.keyFile = domain + ".key"
}
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
if err != nil {
return err
}
@@ -139,7 +138,7 @@ func runCert(ctx context.Context, args []string) error {
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
if filename == "-" {
Stdout.Write(contents)
os.Stdout.Write(contents)
return false, nil
}
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {

View File

@@ -33,22 +33,6 @@ import (
"tailscale.com/version/distro"
)
var Stderr io.Writer = os.Stderr
var Stdout io.Writer = os.Stdout
func printf(format string, a ...any) {
fmt.Fprintf(Stdout, format, a...)
}
// outln is like fmt.Println in the common case, except when Stdout is
// changed (as in js/wasm).
//
// It's not named println because that looks like the Go built-in
// which goes to stderr and formats slightly differently.
func outln(a ...any) {
fmt.Fprintln(Stdout, a...)
}
// ActLikeCLI reports whether a GUI application should act like the
// CLI based on os.Args, GOOS, the context the process is running in
// (pty, parent PID), etc.
@@ -95,16 +79,6 @@ func ActLikeCLI() bool {
return false
}
func newFlagSet(name string) *flag.FlagSet {
onError := flag.ExitOnError
if runtime.GOOS == "js" {
onError = flag.ContinueOnError
}
fs := flag.NewFlagSet(name, onError)
fs.SetOutput(Stderr)
return fs
}
// CleanUpArgs rewrites command line arguments for simplicity and backwards compatibility.
// In particular, it rewrites --authkey to --auth-key.
func CleanUpArgs(args []string) []string {
@@ -129,6 +103,8 @@ var localClient tailscale.LocalClient
// Run runs the CLI. The args do not include the binary name.
func Run(args []string) (err error) {
args = CleanUpArgs(args)
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
args = []string{"version"}
}
@@ -136,11 +112,11 @@ func Run(args []string) (err error) {
var warnOnce sync.Once
tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
warnOnce.Do(func() {
fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
})
})
rootfs := newFlagSet("tailscale")
rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError)
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket")
rootCmd := &ffcli.Command{

View File

@@ -9,13 +9,13 @@ import (
"encoding/json"
"flag"
"fmt"
"net/netip"
"reflect"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tstest"
@@ -56,7 +56,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
flags []string // argv to be parsed by FlagSet
curPrefs *ipn.Prefs
curExitNodeIP netaddr.IP
curExitNodeIP netip.Addr
curUser string // os.Getenv("USER") on the client side
goos string // empty means "linux"
distro distro.Distro
@@ -152,10 +152,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("10.0.42.0/24"),
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
},
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
@@ -168,10 +168,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("10.0.42.0/24"),
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
},
want: "",
@@ -184,10 +184,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.42.0/24"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("10.0.42.0/24"),
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
},
want: "",
@@ -212,8 +212,8 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("1.2.0.0/16"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
@@ -226,10 +226,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
netip.MustParsePrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
@@ -255,16 +255,16 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/16"),
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
@@ -280,14 +280,14 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
ControlURL: ipn.DefaultControlURL,
RouteAll: true,
AllowSingleHosts: false,
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
CorpDNS: false,
ShieldsUp: true,
AdvertiseTags: []string{"tag:foo", "tag:bar"},
Hostname: "myhostname",
ForceDaemon: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("10.0.0.0/16"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/16"),
},
NetfilterMode: preftype.NetfilterNoDivert,
OperatorUser: "alice",
@@ -344,10 +344,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
netip.MustParsePrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
@@ -360,10 +360,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
netaddr.MustParseIPPrefix("1.2.0.0/16"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
netip.MustParsePrefix("1.2.0.0/16"),
},
},
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
@@ -391,14 +391,14 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
ExitNodeIP: netip.MustParseAddr("100.64.5.4"),
},
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
},
{
name: "error_exit_node_omit_with_id_pref",
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
curExitNodeIP: netip.MustParseAddr("100.64.5.7"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
@@ -412,7 +412,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
curPrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
AllowSingleHosts: true,
@@ -493,14 +493,16 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
if err != nil {
t.Fatal(err)
}
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
var got string
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
upEnv := upCheckEnv{
goos: goos,
flagSet: flagSet,
curExitNodeIP: tt.curExitNodeIP,
distro: tt.distro,
}); err != nil {
user: tt.curUser,
}
applyImplicitPrefs(newPrefs, tt.curPrefs, upEnv)
var got string
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upEnv); err != nil {
got = err.Error()
}
if strings.TrimSpace(got) != tt.want {
@@ -560,9 +562,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
WantRunning: true,
AllowSingleHosts: true,
CorpDNS: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
NetfilterMode: preftype.NetfilterOn,
},
@@ -629,7 +631,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
exitNodeIP: "100.105.106.107",
},
st: &ipnstate.Status{
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
TailscaleIPs: []netip.Addr{netip.MustParseAddr("100.105.106.107")},
},
wantErr: `cannot use 100.105.106.107 as an exit node as it is a local IP address to this machine; did you mean --advertise-exit-node?`,
},
@@ -669,8 +671,8 @@ func TestPrefsFromUpArgs(t *testing.T) {
want: &ipn.Prefs{
WantRunning: true,
NoSNAT: true,
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
},
},
},
@@ -784,6 +786,10 @@ func TestUpdatePrefs(t *testing.T) {
curPrefs *ipn.Prefs
env upCheckEnv // empty goos means "linux"
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
// updatePrefs might've mutated them (from applyImplicitPrefs).
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
wantSimpleUp bool
wantJustEditMP *ipn.MaskedPrefs
wantErrSubtr string
@@ -885,6 +891,28 @@ func TestUpdatePrefs(t *testing.T) {
},
env: upCheckEnv{backendState: "Running"},
},
{
// Issue 3808: explicitly empty --operator= should clear value.
name: "explicit_empty_operator",
flags: []string{"--operator="},
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
CorpDNS: true,
AllowSingleHosts: true,
NetfilterMode: preftype.NetfilterOn,
OperatorUser: "somebody",
},
env: upCheckEnv{user: "somebody", backendState: "Running"},
wantJustEditMP: &ipn.MaskedPrefs{
OperatorUserSet: true,
WantRunningSet: true,
},
checkUpdatePrefsMutations: func(t *testing.T, prefs *ipn.Prefs) {
if prefs.OperatorUser != "" {
t.Errorf("operator sent to backend should be empty; got %q", prefs.OperatorUser)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -909,6 +937,9 @@ func TestUpdatePrefs(t *testing.T) {
}
t.Fatal(err)
}
if tt.checkUpdatePrefsMutations != nil {
tt.checkUpdatePrefsMutations(t, newPrefs)
}
if simpleUp != tt.wantSimpleUp {
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
}
@@ -925,7 +956,7 @@ func TestUpdatePrefs(t *testing.T) {
}
}
var cmpIP = cmp.Comparer(func(a, b netaddr.IP) bool {
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
return a == b
})

View File

@@ -31,7 +31,7 @@ permission to use it.
See: https://tailscale.com/kb/1152/synology-outbound/
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host")
fs := flag.NewFlagSet("configure-host", flag.ExitOnError)
return fs
})(),
}

View File

@@ -15,6 +15,9 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"os"
"runtime"
"strconv"
@@ -22,12 +25,14 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/control/controlhttp"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
var debugCmd = &ffcli.Command{
@@ -35,7 +40,7 @@ var debugCmd = &ffcli.Command{
Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
@@ -58,7 +63,7 @@ var debugCmd = &ffcli.Command{
Exec: runDaemonMetrics,
ShortHelp: "print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics")
fs := flag.NewFlagSet("metrics", flag.ExitOnError)
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
return fs
})(),
@@ -98,7 +103,7 @@ var debugCmd = &ffcli.Command{
Exec: runPrefs,
ShortHelp: "print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs := flag.NewFlagSet("prefs", flag.ExitOnError)
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
return fs
})(),
@@ -108,7 +113,7 @@ var debugCmd = &ffcli.Command{
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs := flag.NewFlagSet("watch-ipn", flag.ExitOnError)
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
return fs
})(),
@@ -118,6 +123,17 @@ var debugCmd = &ffcli.Command{
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
{
Name: "ts2021",
Exec: runTS2021,
ShortHelp: "debug ts2021 protocol connectivity",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("ts2021", flag.ExitOnError)
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version")
return fs
})(),
},
},
}
@@ -130,7 +146,7 @@ var debugArgs struct {
func writeProfile(dst string, v []byte) error {
if dst == "-" {
_, err := Stdout.Write(v)
_, err := os.Stdout.Write(v)
return err
}
return os.WriteFile(dst, v, 0600)
@@ -182,7 +198,7 @@ func runDebug(ctx context.Context, args []string) error {
if err != nil {
fatalf("%v\n", err)
}
e := json.NewEncoder(Stdout)
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
e.Encode(wfs)
return nil
@@ -196,7 +212,7 @@ func runDebug(ctx context.Context, args []string) error {
return err
}
log.Printf("Size: %v\n", size)
io.Copy(Stdout, rc)
io.Copy(os.Stdout, rc)
return nil
}
if usedFlag {
@@ -210,14 +226,14 @@ func runDebug(ctx context.Context, args []string) error {
func runLocalCreds(ctx context.Context, args []string) error {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
fmt.Printf("curl http://localhost:%v/localapi/v0/status\n", safesocket.WindowsLocalPort)
return nil
}
printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
@@ -231,10 +247,10 @@ func runPrefs(ctx context.Context, args []string) error {
return err
}
if prefsArgs.pretty {
outln(prefs.Pretty())
fmt.Println(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
outln(string(j))
fmt.Println(string(j))
}
return nil
}
@@ -252,7 +268,7 @@ func runWatchIPN(ctx context.Context, args []string) error {
n.NetMap = nil
}
j, _ := json.MarshalIndent(n, "", "\t")
printf("%s\n", j)
fmt.Printf("%s\n", j)
})
bc.RequestEngineStatus()
pump(ctx, bc, c)
@@ -266,7 +282,7 @@ func runDERPMap(ctx context.Context, args []string) error {
"failed to get local derp map, instead `curl %s/derpmap/default`: %w", ipn.DefaultControlURL, err,
)
}
enc := json.NewEncoder(Stdout)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
enc.Encode(dm)
return nil
@@ -283,7 +299,7 @@ func localAPIAction(action string) func(context.Context, []string) error {
func runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)
fmt.Println(e)
}
return nil
}
@@ -322,7 +338,7 @@ func runDaemonGoroutines(ctx context.Context, args []string) error {
if err != nil {
return err
}
Stdout.Write(goroutines)
os.Stdout.Write(goroutines)
return nil
}
@@ -338,7 +354,7 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
return err
}
if !metricsArgs.watch {
Stdout.Write(out)
os.Stdout.Write(out)
return nil
}
bs := bufio.NewScanner(bytes.NewReader(out))
@@ -375,9 +391,9 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
if len(changes) > 0 {
format := fmt.Sprintf("%%-%ds %%+5d => %%v\n", maxNameLen)
for _, c := range changes {
fmt.Fprintf(Stdout, format, c.name, c.to-c.from, c.to)
fmt.Fprintf(os.Stdout, format, c.name, c.to-c.from, c.to)
}
io.WriteString(Stdout, "\n")
io.WriteString(os.Stdout, "\n")
}
time.Sleep(time.Second)
}
@@ -388,23 +404,23 @@ func runVia(ctx context.Context, args []string) error {
default:
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
case 1:
ipp, err := netaddr.ParseIPPrefix(args[0])
ipp, err := netip.ParsePrefix(args[0])
if err != nil {
return err
}
if !ipp.IP().Is6() {
if !ipp.Addr().Is6() {
return errors.New("with one argument, expect an IPv6 CIDR")
}
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
if !tsaddr.TailscaleViaRange().Contains(ipp.Addr()) {
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()
v4 := tsaddr.UnmapVia(ipp.Addr())
a := ipp.Addr().As16()
siteID := binary.BigEndian.Uint32(a[8:12])
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netip.PrefixFrom(v4, ipp.Bits()-96))
case 2:
siteID, err := strconv.ParseUint(args[0], 0, 32)
if err != nil {
@@ -413,7 +429,7 @@ func runVia(ctx context.Context, args []string) error {
if siteID > 0xff {
return fmt.Errorf("site-id values over 255 are currently reserved")
}
ipp, err := netaddr.ParseIPPrefix(args[1])
ipp, err := netip.ParsePrefix(args[1])
if err != nil {
return err
}
@@ -425,3 +441,67 @@ func runVia(ctx context.Context, args []string) error {
}
return nil
}
var ts2021Args struct {
host string // "controlplane.tailscale.com"
version int // 27 or whatever
}
func runTS2021(ctx context.Context, args []string) error {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ltime | log.Lmicroseconds)
machinePrivate := key.NewMachine()
var dialer net.Dialer
var keys struct {
PublicKey key.MachinePublic
}
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
log.Printf("Fetching keys from %s ...", keysURL)
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Do: %v", err)
return err
}
if res.StatusCode != 200 {
log.Printf("Status: %v", res.Status)
return errors.New(res.Status)
}
if err := json.NewDecoder(res.Body).Decode(&keys); err != nil {
log.Printf("JSON: %v", err)
return fmt.Errorf("decoding /keys JSON: %w", err)
}
res.Body.Close()
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
log.Printf("Dial(%q, %q) ...", network, address)
c, err := dialer.DialContext(ctx, network, address)
if err != nil {
log.Printf("Dial(%q, %q) = %v", network, address, err)
} else {
log.Printf("Dial(%q, %q) = %v / %v", network, address, c.LocalAddr(), c.RemoteAddr())
}
return c, err
}
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
log.Printf("controlhttp.Dial = %p, %v", conn, err)
if err != nil {
return err
}
log.Printf("did noise handshake")
gotPeer := conn.Peer()
if gotPeer != keys.PublicKey {
log.Printf("peer = %v, want %v", gotPeer, keys.PublicKey)
return errors.New("key mismatch")
}
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
return nil
}

View File

@@ -6,7 +6,9 @@ package cli
import (
"context"
"flag"
"fmt"
"os"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
@@ -17,7 +19,14 @@ var downCmd = &ffcli.Command{
ShortUsage: "down",
ShortHelp: "Disconnect from Tailscale",
Exec: runDown,
Exec: runDown,
FlagSet: newDownFlagSet(),
}
func newDownFlagSet() *flag.FlagSet {
downf := flag.NewFlagSet("down", flag.ExitOnError)
registerAcceptRiskFlag(downf)
return downf
}
func runDown(ctx context.Context, args []string) error {
@@ -25,12 +34,18 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
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)
}
if st.BackendState == "Stopped" {
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
return nil
}
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{

View File

@@ -14,6 +14,7 @@ import (
"log"
"mime"
"net/http"
"net/netip"
"os"
"path"
"path/filepath"
@@ -23,7 +24,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -54,7 +54,7 @@ var fileCpCmd = &ffcli.Command{
ShortHelp: "Copy file(s) to a host",
Exec: runCp,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cp")
fs := flag.NewFlagSet("cp", flag.ExitOnError)
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
@@ -85,7 +85,7 @@ func runCp(ctx context.Context, args []string) error {
hadBrackets = true
target = strings.TrimSuffix(strings.TrimPrefix(target, "["), "]")
}
if ip, err := netaddr.ParseIP(target); err == nil && ip.Is6() && !hadBrackets {
if ip, err := netip.ParseAddr(target); err == nil && ip.Is6() && !hadBrackets {
return fmt.Errorf("an IPv6 literal must be written as [%s]", ip)
} else if hadBrackets && (err != nil || !ip.Is6()) {
return errors.New("unexpected brackets around target")
@@ -100,7 +100,7 @@ func runCp(ctx context.Context, args []string) error {
return fmt.Errorf("can't send to %s: %v", target, err)
}
if isOffline {
fmt.Fprintf(Stderr, "# warning: %s is offline\n", target)
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
}
if len(files) > 1 {
@@ -168,7 +168,7 @@ func runCp(ctx context.Context, args []string) error {
}
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
ip, err := netaddr.ParseIP(ipStr)
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return "", false, err
}
@@ -179,7 +179,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.IP() != ip {
if a.Addr() != ip {
continue
}
isOffline = n.Online != nil && !*n.Online
@@ -191,7 +191,7 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
// fileTargetErrorDetail returns a non-nil error saying why ip is an
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
func fileTargetErrorDetail(ctx context.Context, ip netip.Addr) error {
found := false
if st, err := localClient.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
@@ -281,7 +281,7 @@ func runCpTargets(ctx context.Context, args []string) error {
if detail != "" {
detail = "\t" + detail
}
printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
fmt.Printf("%s\t%s%s\n", n.Addresses[0].Addr(), n.ComputedName, detail)
}
return nil
}
@@ -315,7 +315,7 @@ var fileGetCmd = &ffcli.Command{
ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("get")
fs := flag.NewFlagSet("get", flag.ExitOnError)
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")
@@ -415,7 +415,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
break
}
if getArgs.verbose {
printf("waiting for file...")
fmt.Printf("waiting for file...")
}
if err := waitForFile(ctx); err != nil {
errs = append(errs, err)
@@ -436,7 +436,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
continue
}
if getArgs.verbose {
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
fmt.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))
@@ -448,7 +448,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
// 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))
fmt.Printf("moved %d/%d files\n", deleted, len(wfs))
}
return errs
}
@@ -471,7 +471,7 @@ func runFileGet(ctx context.Context, args []string) error {
for {
errs := runFileGetOneBatch(ctx, dir)
for _, err := range errs {
outln(err)
fmt.Println(err)
}
if len(errs) > 0 {
// It's possible whatever caused the error(s) (e.g. conflicting target file,
@@ -493,7 +493,7 @@ func runFileGet(ctx context.Context, args []string) error {
return nil
}
for _, err := range errs[:len(errs)-1] {
outln(err)
fmt.Println(err)
}
return errs[len(errs)-1]
}

View File

@@ -9,9 +9,9 @@ import (
"errors"
"flag"
"fmt"
"net/netip"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/ipn/ipnstate"
)
@@ -22,7 +22,7 @@ var ipCmd = &ffcli.Command{
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ip")
fs := flag.NewFlagSet("ip", flag.ExitOnError)
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
@@ -85,7 +85,7 @@ func runIP(ctx context.Context, args []string) error {
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {
match = true
outln(ip)
fmt.Println(ip)
}
}
if !match {
@@ -100,7 +100,7 @@ func runIP(ctx context.Context, args []string) error {
}
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
ip, err := netaddr.ParseIP(ipStr)
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return
}

View File

@@ -29,7 +29,7 @@ func runNC(ctx context.Context, args []string) error {
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
fmt.Printf("%s\n", description)
os.Exit(1)
}

View File

@@ -13,6 +13,7 @@ import (
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"strings"
"time"
@@ -32,7 +33,7 @@ var netcheckCmd = &ffcli.Command{
ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netcheck")
fs := flag.NewFlagSet("netcheck", flag.ExitOnError)
fs.StringVar(&netcheckArgs.format, "format", "", `output format; empty (for human-readable), "json" or "json-line"`)
fs.DurationVar(&netcheckArgs.every, "every", 0, "if non-zero, do an incremental report with the given frequency")
fs.BoolVar(&netcheckArgs.verbose, "verbose", false, "verbose logs")
@@ -59,7 +60,7 @@ func runNetcheck(ctx context.Context, args []string) error {
}
if strings.HasPrefix(netcheckArgs.format, "json") {
fmt.Fprintln(Stderr, "# Warning: this JSON format is not yet considered a stable interface")
fmt.Fprintln(os.Stderr, "# Warning: this JSON format is not yet considered a stable interface")
}
dm, err := localClient.CurrentDERPMap(ctx)
@@ -111,36 +112,38 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
}
if j != nil {
j = append(j, '\n')
Stdout.Write(j)
os.Stdout.Write(j)
return nil
}
printf("\nReport:\n")
printf("\t* UDP: %v\n", report.UDP)
fmt.Printf("\nReport:\n")
fmt.Printf("\t* UDP: %v\n", report.UDP)
if report.GlobalV4 != "" {
printf("\t* IPv4: yes, %v\n", report.GlobalV4)
fmt.Printf("\t* IPv4: yes, %v\n", report.GlobalV4)
} else {
printf("\t* IPv4: (no addr found)\n")
fmt.Printf("\t* IPv4: (no addr found)\n")
}
if report.GlobalV6 != "" {
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
fmt.Printf("\t* IPv6: yes, %v\n", report.GlobalV6)
} else if report.IPv6 {
printf("\t* IPv6: (no addr found)\n")
fmt.Printf("\t* IPv6: (no addr found)\n")
} else if report.OSHasIPv6 {
fmt.Printf("\t* IPv6: no, but OS has support\n")
} else {
printf("\t* IPv6: no\n")
fmt.Printf("\t* IPv6: no, unavailable in OS\n")
}
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
printf("\t* HairPinning: %v\n", report.HairPinning)
printf("\t* PortMapping: %v\n", portMapping(report))
fmt.Printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
fmt.Printf("\t* HairPinning: %v\n", report.HairPinning)
fmt.Printf("\t* PortMapping: %v\n", portMapping(report))
// When DERP latency checking failed,
// magicsock will try to pick the DERP server that
// most of your other nodes are also using
if len(report.RegionLatency) == 0 {
printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
} else {
printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
printf("\t* DERP latency:\n")
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
fmt.Printf("\t* DERP latency:\n")
var rids []int
for rid := range dm.Regions {
rids = append(rids, rid)
@@ -167,7 +170,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
if netcheckArgs.verbose {
derpNum = fmt.Sprintf("derp%d, ", rid)
}
printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
}
}
return nil

View File

@@ -11,12 +11,12 @@ import (
"fmt"
"log"
"net"
"net/netip"
"os"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
@@ -46,11 +46,12 @@ relay node.
`),
Exec: runPing,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ping")
fs := flag.NewFlagSet("ping", flag.ExitOnError)
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 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
@@ -63,6 +64,7 @@ var pingArgs struct {
verbose bool
tsmp bool
icmp bool
peerAPI bool
timeout time.Duration
}
@@ -73,6 +75,9 @@ func pingType() tailcfg.PingType {
if pingArgs.icmp {
return tailcfg.PingICMP
}
if pingArgs.peerAPI {
return tailcfg.PingPeerAPI
}
return tailcfg.PingDisco
}
@@ -83,7 +88,7 @@ func runPing(ctx context.Context, args []string) error {
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
fmt.Printf("%s\n", description)
os.Exit(1)
}
@@ -98,7 +103,7 @@ func runPing(ctx context.Context, args []string) error {
return err
}
if self {
printf("%v is local Tailscale IP\n", ip)
fmt.Printf("%v is local Tailscale IP\n", ip)
return nil
}
@@ -111,18 +116,24 @@ func runPing(ctx context.Context, args []string) error {
for {
n++
ctx, cancel := context.WithTimeout(ctx, pingArgs.timeout)
pr, err := localClient.Ping(ctx, netaddr.MustParseIP(ip), pingType())
pr, err := localClient.Ping(ctx, netip.MustParseAddr(ip), pingType())
cancel()
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
printf("ping %q timed out\n", ip)
fmt.Printf("ping %q timed out\n", ip)
if n == pingArgs.num {
if !anyPong {
return errors.New("no reply")
}
return nil
}
continue
}
return err
}
if pr.Err != "" {
if pr.IsLocalIP {
outln(pr.Err)
fmt.Println(pr.Err)
return nil
}
return errors.New(pr.Err)
@@ -137,12 +148,16 @@ func runPing(ctx context.Context, args []string) error {
// For now just say which protocol it used.
via = string(pingType())
}
if pingArgs.peerAPI {
fmt.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)
fmt.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
}

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
}

View File

@@ -10,18 +10,18 @@ import (
"errors"
"fmt"
"log"
"net/netip"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"syscall"
"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{
@@ -32,6 +32,9 @@ var sshCmd = &ffcli.Command{
}
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>")
}
@@ -59,7 +62,7 @@ func runSSH(ctx context.Context, args []string) error {
hostForSSH = v
}
ssh, err := exec.LookPath("ssh")
ssh, err := findSSH()
if err != nil {
// TODO(bradfitz): use Go's crypto/ssh client instead
// of failing. But for now:
@@ -116,24 +119,7 @@ func runSSH(ctx context.Context, args []string) error {
log.Printf("Running: %q, %q ...", ssh, argv)
}
if runtime.GOOS == "windows" {
// Don't use syscall.Exec on Windows.
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
}
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
return err
}
return errors.New("unreachable")
return execSSH(ssh, argv)
}
func writeKnownHosts(st *ipnstate.Status) (knownHostsFile string, err error) {
@@ -177,10 +163,10 @@ func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok boo
if arg == "" {
return
}
argIP, _ := netaddr.ParseIP(arg)
argIP, _ := netip.ParseAddr(arg)
for _, ps := range st.Peer {
dnsName = ps.DNSName
if !argIP.IsZero() {
if argIP.IsValid() {
for _, ip := range ps.TailscaleIPs {
if ip == argIP {
return dnsName, true
@@ -197,3 +183,28 @@ func nodeDNSNameFromArg(st *ipnstate.Status, arg string) (dnsName string, ok boo
}
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 := netip.ParseAddr(ipStr)
if err != nil {
return false
}
return tsaddr.IsTailscaleIP(ip)
}

View File

@@ -0,0 +1,26 @@
// 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 !windows
// +build !windows
package cli
import (
"errors"
"os"
"os/exec"
"syscall"
)
func findSSH() (string, error) {
return exec.LookPath("ssh")
}
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

@@ -0,0 +1,38 @@
// 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"
"path/filepath"
)
func findSSH() (string, error) {
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
// occured with ssh.exe provided by msys2/cygwin and other environments.
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
if st, err := os.Stat(exe); err == nil && !st.IsDir() {
return exe, nil
}
}
return exec.LookPath("ssh")
}
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

@@ -13,12 +13,12 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"os"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
@@ -46,7 +46,7 @@ https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
`),
Exec: runStatus,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("status")
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
@@ -92,7 +92,7 @@ func runStatus(ctx context.Context, args []string) error {
if err != nil {
return err
}
printf("%s", j)
fmt.Printf("%s", j)
return nil
}
if statusArgs.web {
@@ -101,7 +101,7 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
statusURL := interfaces.HTTPOfListener(ln)
printf("Serving Tailscale status at %v ...\n", statusURL)
fmt.Printf("Serving Tailscale status at %v ...\n", statusURL)
go func() {
<-ctx.Done()
ln.Close()
@@ -128,18 +128,20 @@ 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 {
fmt.Printf("# Health check:\n")
for _, m := range st.Health {
fmt.Printf("# - %s\n", m)
}
fmt.Println()
}
if len(st.Health) > 0 {
printf("# Health check:\n")
for _, m := range st.Health {
printf("# - %s\n", m)
}
outln()
description, ok := isRunningOrStarting(st)
if !ok {
fmt.Println(description)
os.Exit(1)
}
var buf bytes.Buffer
@@ -211,7 +213,7 @@ func runStatus(ctx context.Context, args []string) error {
printPS(ps)
}
}
Stdout.Write(buf.Bytes())
os.Stdout.Write(buf.Bytes())
return nil
}
@@ -258,7 +260,7 @@ func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
return u.LoginName
}
func firstIPString(v []netaddr.IP) string {
func firstIPString(v []netip.Addr) string {
if len(v) == 0 {
return ""
}

View File

@@ -13,17 +13,18 @@ import (
"flag"
"fmt"
"log"
"net/netip"
"os"
"reflect"
"runtime"
"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/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
@@ -84,7 +85,7 @@ var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
func inTest() bool { return flag.Lookup("test.v") != nil }
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf := newFlagSet("up")
upf := flag.NewFlagSet("up", flag.ExitOnError)
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
@@ -114,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
}
@@ -146,6 +149,7 @@ type upArgsT struct {
hostname string
opUser string
json bool
timeout time.Duration
}
func (a upArgsT) getAuthKey() (string, error) {
@@ -191,22 +195,22 @@ type upOutputJSON struct {
}
func warnf(format string, args ...any) {
printf("Warning: "+format+"\n", args...)
fmt.Printf("Warning: "+format+"\n", args...)
}
var (
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6default = netaddr.MustParseIPPrefix("::/0")
ipv4default = netip.MustParsePrefix("0.0.0.0/0")
ipv6default = netip.MustParsePrefix("::/0")
)
func validateViaPrefix(ipp netaddr.IPPrefix) error {
func validateViaPrefix(ipp netip.Prefix) 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()
a := ipp.Addr().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.
@@ -219,13 +223,13 @@ func validateViaPrefix(ipp netaddr.IPPrefix) error {
return nil
}
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netaddr.IPPrefix, error) {
routeMap := map[netaddr.IPPrefix]bool{}
func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netip.Prefix, error) {
routeMap := map[netip.Prefix]bool{}
if advertiseRoutes != "" {
var default4, default6 bool
advroutes := strings.Split(advertiseRoutes, ",")
for _, s := range advroutes {
ipp, err := netaddr.ParseIPPrefix(s)
ipp, err := netip.ParsePrefix(s)
if err != nil {
return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s)
}
@@ -251,10 +255,10 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
}
}
if advertiseDefaultRoute {
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
routeMap[netip.MustParsePrefix("0.0.0.0/0")] = true
routeMap[netip.MustParsePrefix("::/0")] = true
}
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
routes := make([]netip.Prefix, 0, len(routeMap))
for r := range routeMap {
routes = append(routes, r)
}
@@ -262,7 +266,7 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
if routes[i].Bits() != routes[j].Bits() {
return routes[i].Bits() < routes[j].Bits()
}
return routes[i].IP().Less(routes[j].IP())
return routes[i].Addr().Less(routes[j].Addr())
})
return routes, nil
}
@@ -358,7 +362,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
// without changing any settings.
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
if !env.upArgs.reset {
applyImplicitPrefs(prefs, curPrefs, env.user)
applyImplicitPrefs(prefs, curPrefs, env)
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
return false, nil, err
@@ -400,7 +404,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return simpleUp, justEditMP, nil
}
func runUp(ctx context.Context, args []string) error {
func runUp(ctx context.Context, args []string) (retErr error) {
if len(args) > 0 {
fatalf("too many non-flag arguments: %q", args)
}
@@ -465,6 +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
}
}
defer func() {
if retErr == nil {
checkSSHUpWarnings(ctx)
}
}()
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil {
fatalf("%s", err)
@@ -516,7 +538,7 @@ func runUp(ctx context.Context, args []string) error {
if env.upArgs.json {
printUpDoneJSON(ipn.NeedsMachineAuth, "")
} else {
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
case ipn.Running:
// Done full authentication process
@@ -524,7 +546,7 @@ func runUp(ctx context.Context, args []string) error {
printUpDoneJSON(ipn.Running, "")
} else if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(Stderr, "Success.\n")
fmt.Fprintf(os.Stderr, "Success.\n")
}
select {
case running <- true:
@@ -553,13 +575,13 @@ func runUp(ctx context.Context, args []string) error {
fmt.Println(string(data))
}
} else {
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
if upArgs.qr {
q, err := qrcode.New(*url, qrcode.Medium)
if err != nil {
log.Printf("QR code error: %v", err)
} else {
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
fmt.Fprintf(os.Stderr, "%s\n", q.ToString(false))
}
}
}
@@ -632,6 +654,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
@@ -649,6 +677,30 @@ 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"`)
}
}
func checkSSHUpWarnings(ctx context.Context) {
if !upArgs.runSSH {
return
}
st, err := localClient.Status(ctx)
if err != nil {
// Ignore. Don't spam more.
return
}
if len(st.Health) == 0 {
return
}
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") {
fmt.Printf("%s\n", st.Health[0])
return
}
fmt.Printf("# Health check:\n")
for _, m := range st.Health {
fmt.Printf(" - %s\n", m)
}
}
@@ -705,7 +757,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
@@ -738,7 +790,7 @@ type upCheckEnv struct {
flagSet *flag.FlagSet
upArgs upArgsT
backendState string
curExitNodeIP netaddr.IP
curExitNodeIP netip.Addr
distro distro.Distro
}
@@ -829,13 +881,20 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
return errors.New(sb.String())
}
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
// this is just the operator user, which only needs to be set if it doesn't
// applyImplicitPrefs mutates prefs to add implicit preferences for the user operator.
// If the operator flag is passed no action is taken, otherwise this only needs to be set if it doesn't
// match the current user.
//
// curUser is os.Getenv("USER"). It's pulled out for testability.
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
explicitOperator := false
env.flagSet.Visit(func(f *flag.Flag) {
if f.Name == "operator" {
explicitOperator = true
}
})
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == env.user && !explicitOperator {
prefs.OperatorUser = oldPrefs.OperatorUser
}
}
@@ -854,10 +913,10 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
ret := make(map[string]any)
exitNodeIPStr := func() string {
if !prefs.ExitNodeIP.IsZero() {
if prefs.ExitNodeIP.IsValid() {
return prefs.ExitNodeIP.String()
}
if prefs.ExitNodeID.IsZero() || env.curExitNodeIP.IsZero() {
if prefs.ExitNodeID.IsZero() || !env.curExitNodeIP.IsValid() {
return ""
}
return env.curExitNodeIP.String()
@@ -932,13 +991,13 @@ func fmtFlagValueArg(flagName string, val any) string {
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(fmt.Sprint(val)))
}
func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
func hasExitNodeRoutes(rr []netip.Prefix) bool {
var v4, v6 bool
for _, r := range rr {
if r.Bits() == 0 {
if r.IP().Is4() {
if r.Addr().Is4() {
v4 = true
} else if r.IP().Is6() {
} else if r.Addr().Is6() {
v6 = true
}
}
@@ -949,11 +1008,11 @@ func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
// withoutExitNodes returns rr unchanged if it has only 1 or 0 /0
// routes. If it has both IPv4 and IPv6 /0 routes, then it returns
// a copy with all /0 routes removed.
func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
func withoutExitNodes(rr []netip.Prefix) []netip.Prefix {
if !hasExitNodeRoutes(rr) {
return rr
}
var out []netaddr.IPPrefix
var out []netip.Prefix
for _, r := range rr {
if r.Bits() > 0 {
out = append(out, r)
@@ -964,11 +1023,11 @@ func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
// exitNodeIP returns the exit node IP from p, using st to map
// it from its ID form to an IP address if needed.
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netaddr.IP) {
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
if p == nil {
return
}
if !p.ExitNodeIP.IsZero() {
if p.ExitNodeIP.IsValid() {
return p.ExitNodeIP
}
id := p.ExitNodeID

View File

@@ -18,7 +18,7 @@ var versionCmd = &ffcli.Command{
ShortUsage: "version [flags]",
ShortHelp: "Print Tailscale version",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("version")
fs := flag.NewFlagSet("version", flag.ExitOnError)
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
return fs
})(),
@@ -34,16 +34,16 @@ func runVersion(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
if !versionArgs.daemon {
outln(version.String())
fmt.Println(version.String())
return nil
}
printf("Client: %s\n", version.String())
fmt.Printf("Client: %s\n", version.String())
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return err
}
printf("Daemon: %s\n", st.Version)
fmt.Printf("Daemon: %s\n", st.Version)
return nil
}

View File

@@ -20,6 +20,7 @@ import (
"net"
"net/http"
"net/http/cgi"
"net/netip"
"net/url"
"os"
"os/exec"
@@ -27,7 +28,6 @@ import (
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
@@ -75,7 +75,7 @@ Tailscale, as opposed to a CLI or a native app.
`),
FlagSet: (func() *flag.FlagSet {
webf := newFlagSet("web")
webf := flag.NewFlagSet("web", flag.ExitOnError)
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
return webf
@@ -208,12 +208,20 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
return "", nil, err
}
token, err := r.Cookie("qtoken")
if err != nil {
return "", nil, err
if err == nil {
return qnapAuthnQtoken(r, user.Value, token.Value)
}
sid, err := r.Cookie("NAS_SID")
if err == nil {
return qnapAuthnSid(r, user.Value, sid.Value)
}
return "", nil, fmt.Errorf("not authenticated by any mechanism")
}
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
query := url.Values{
"qtoken": []string{token.Value},
"user": []string{user.Value},
"qtoken": []string{token},
"user": []string{user},
}
u := url.URL{
Scheme: r.URL.Scheme,
@@ -221,7 +229,26 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
resp, err := http.Get(u.String())
return qnapAuthnFinish(user, u.String())
}
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
query := url.Values{
"sid": []string{sid},
}
u := url.URL{
Scheme: r.URL.Scheme,
Host: r.URL.Host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
}
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
resp, err := http.Get(url)
if err != nil {
return "", nil, err
}
@@ -237,7 +264,7 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
if authResp.AuthPassed == 0 {
return "", nil, fmt.Errorf("not authenticated")
}
return user.Value, authResp, nil
return user, authResp, nil
}
func synoAuthn() (string, error) {
@@ -366,8 +393,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
Status: st.BackendState,
DeviceName: deviceName,
}
exitNodeRouteV4 := netaddr.MustParseIPPrefix("0.0.0.0/0")
exitNodeRouteV6 := netaddr.MustParseIPPrefix("::/0")
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
for _, r := range prefs.AdvertiseRoutes {
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
data.AdvertiseExitNode = true

View File

@@ -1,14 +1,18 @@
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware)
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
L github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/flate from nhooyr.io/websocket
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
@@ -26,47 +30,50 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/derp+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
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+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
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+
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netaddr from tailscale.com/disco+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
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/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/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+
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+
tailscale.com/tka from tailscale.com/types/key
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
@@ -76,6 +83,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/nettype from tailscale.com/net/netcheck+
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/ipn
@@ -83,23 +91,26 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/views from tailscale.com/tailcfg+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
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
tailscale.com/util/singleflight from tailscale.com/net/dnscache
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
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls
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+
golang.org/x/crypto/hkdf from crypto/tls
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/salsa20/salsa from golang.org/x/crypto/nacl/box+
@@ -112,7 +123,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
@@ -152,6 +162,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
@@ -172,7 +183,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
io from bufio+
io/fs from crypto/rand+
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
log from expvar+
math from compress/flate+
@@ -187,7 +198,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/http/cgi from tailscale.com/cmd/tailscale/cli
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/netip from net
net/netip from net+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
@@ -199,7 +210,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from crypto/x509+
regexp from github.com/tailscale/goupnp/httpu+
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight+
runtime/debug from tailscale.com/util/singleflight+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+

View File

@@ -17,7 +17,6 @@ import (
func main() {
args := os.Args[1:]
args = cli.CleanUpArgs(args)
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
args = []string{"web", "-cgi"}
}

View File

@@ -20,12 +20,12 @@ import (
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"os"
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -266,11 +266,11 @@ func debugPortmap(ctx context.Context) error {
return err
}
gatewayAndSelfIP := func() (gw, self netaddr.IP, ok bool) {
gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netaddr.MustParseIP(v[:i])
self = netaddr.MustParseIP(v[i+1:])
gw = netip.MustParseAddr(v[:i])
self = netip.MustParseAddr(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()

View File

@@ -1,5 +1,7 @@
tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/depaware)
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
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
@@ -62,11 +64,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
@@ -76,9 +80,10 @@ 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
@@ -112,9 +117,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
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/unsafe/assume-no-moving-gc from go4.org/intern
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/control/controlbase+
go4.org/netipx from tailscale.com/ipn/ipnlocal+
W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun
💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/conn/winrio from golang.zx2c4.com/wireguard/conn
@@ -128,7 +133,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 golang.zx2c4.com/wireguard/tun from golang.zx2c4.com/wireguard/device+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2
💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
@@ -140,9 +146,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 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/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+
@@ -151,48 +156,47 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/multicast from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
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/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
@@ -203,42 +207,44 @@ 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/netaddr from tailscale.com/disco+
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/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/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
tailscale.com/tka from tailscale.com/types/key
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+
@@ -248,17 +254,18 @@ 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/nettype from tailscale.com/wgengine/magicsock+
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/control/controlclient+
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+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
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+
@@ -266,30 +273,30 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
tailscale.com/util/netconv from tailscale.com/wgengine/magicsock
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
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/singleflight from tailscale.com/control/controlclient+
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/cmd/tailscaled+
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
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
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/argon2 from tailscale.com/tka
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+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
@@ -317,11 +324,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
golang.org/x/sync/singleflight from tailscale.com/control/controlclient+
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
@@ -355,30 +361,31 @@ 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/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
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/fs from crypto/x509+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+
@@ -395,19 +402,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/pprof from tailscale.com/log/logheap+
runtime/trace from net/http/pprof
sort from compress/flate+
strconv from compress/flate+

View File

@@ -21,6 +21,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"net/netip"
"os"
"os/signal"
"path/filepath"
@@ -29,7 +30,6 @@ import (
"syscall"
"time"
"inet.af/netaddr"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
@@ -128,6 +128,8 @@ var subCommands = map[string]*func([]string) error{
"be-child": &beChildFunc,
}
var beCLI func() // non-nil if CLI is linked in
func main() {
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
@@ -137,12 +139,17 @@ 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")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
beCLI()
return
}
if len(os.Args) > 1 {
sub := os.Args[1]
if fp, ok := subCommands[sub]; ok {
@@ -158,13 +165,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" && flag.Arg(0) != "/firewall") {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
}
}
if printVersion {
@@ -187,6 +193,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)
@@ -258,13 +274,6 @@ func ipnServerOpts() (o ipnserver.Options) {
}
switch goos {
case "js":
// The js/wasm client has no state storage so for now
// treat all interactive logins as ephemeral.
// TODO(bradfitz): if we start using browser LocalStorage
// or something, then rethink this.
o.LoginFlags = controlclient.LoginEphemeral
fallthrough
default:
o.SurviveDisconnects = true
o.AutostartStateKey = ipn.GlobalDaemonStateKey
@@ -357,11 +366,11 @@ func run() error {
ns.ProcessSubnets = useNetstack || wrapNetstack
if useNetstack {
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
_, ok := e.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
}
@@ -395,7 +404,6 @@ 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)
@@ -428,6 +436,7 @@ func run() error {
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
}
defer dialer.Close()
err = srv.Run(ctx, ln)
// Cancelation is not an error: it is the only way to stop ipnserver.
@@ -506,7 +515,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
} else {
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
tstun.Diagnose(logf, name, err)
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev

View File

@@ -25,6 +25,7 @@ import (
"encoding/json"
"fmt"
"log"
"net/netip"
"os"
"time"
@@ -32,7 +33,6 @@ import (
"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"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
@@ -245,7 +245,7 @@ func beFirewallKillswitch() bool {
// is passed in via stdin encoded in json.
dcd := json.NewDecoder(os.Stdin)
for {
var routes []netaddr.IPPrefix
var routes []netip.Prefix
if err := dcd.Decode(&routes); err != nil {
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
}
@@ -260,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)
@@ -281,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,
@@ -294,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()
@@ -319,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",
@@ -332,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
@@ -344,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
@@ -362,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

@@ -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.
//go:build ts_include_cli
// +build ts_include_cli
package main
import (
"fmt"
"os"
"tailscale.com/cmd/tailscale/cli"
)
func init() {
beCLI = func() {
args := os.Args[1:]
if err := cli.Run(args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}

4
cmd/tsconnect/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
src/wasm_exec.js
src/main.wasm
node_modules/
dist/

195
cmd/tsconnect/build.go Normal file
View File

@@ -0,0 +1,195 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"github.com/andybalholm/brotli"
esbuild "github.com/evanw/esbuild/pkg/api"
"golang.org/x/sync/errgroup"
)
func runBuild() {
buildOptions, err := commonSetup(prodMode)
if err != nil {
log.Fatalf("Cannot setup: %v", err)
}
log.Printf("Linting...\n")
if err := runYarn("lint"); err != nil {
log.Fatalf("Linting failed: %v", err)
}
if err := cleanDist(); err != nil {
log.Fatalf("Cannot clean %s: %v", *distDir, err)
}
buildOptions.Write = true
buildOptions.MinifyWhitespace = true
buildOptions.MinifyIdentifiers = true
buildOptions.MinifySyntax = true
buildOptions.EntryNames = "[dir]/[name]-[hash]"
buildOptions.AssetNames = "[name]-[hash]"
buildOptions.Metafile = true
log.Printf("Running esbuild...\n")
result := esbuild.Build(*buildOptions)
if len(result.Errors) > 0 {
log.Printf("ESBuild Error:\n")
for _, e := range result.Errors {
log.Printf("%v", e)
}
log.Fatal("Build failed")
}
if len(result.Warnings) > 0 {
log.Printf("ESBuild Warnings:\n")
for _, w := range result.Warnings {
log.Printf("%v", w)
}
}
// Preserve build metadata so we can extract hashed file names for serving.
metadataBytes, err := fixEsbuildMetadataPaths(result.Metafile)
if err != nil {
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
}
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
log.Fatalf("Cannot write metadata: %v", err)
}
if er := precompressDist(); err != nil {
log.Fatalf("Cannot precompress resources: %v", er)
}
}
// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
// relative to the dist directory (it normally uses paths relative to the cwd,
// which are akward if we're running with a different cwd at serving time).
func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
var metadata EsbuildMetadata
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
return nil, fmt.Errorf("Cannot parse metadata: %w", err)
}
distAbsPath, err := filepath.Abs(*distDir)
if err != nil {
return nil, fmt.Errorf("Cannot get absolute path from %s: %w", *distDir, err)
}
for outputPath, output := range metadata.Outputs {
outputAbsPath, err := filepath.Abs(outputPath)
if err != nil {
return nil, fmt.Errorf("Cannot get absolute path from %s: %w", outputPath, err)
}
outputRelPath, err := filepath.Rel(distAbsPath, outputAbsPath)
if err != nil {
return nil, fmt.Errorf("Cannot get relative path from %s: %w", outputRelPath, err)
}
delete(metadata.Outputs, outputPath)
metadata.Outputs[outputRelPath] = output
}
return json.Marshal(metadata)
}
// cleanDist removes files from the dist build directory, except the placeholder
// one that we keep to make sure Git still creates the directory.
func cleanDist() error {
log.Printf("Cleaning %s...\n", *distDir)
files, err := os.ReadDir(*distDir)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(*distDir, 0755)
}
return err
}
for _, file := range files {
if file.Name() != "placeholder" {
if err := os.Remove(filepath.Join(*distDir, file.Name())); err != nil {
return err
}
}
}
return nil
}
func precompressDist() error {
log.Printf("Pre-compressing files in %s/...\n", *distDir)
var eg errgroup.Group
err := fs.WalkDir(os.DirFS(*distDir), ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !compressibleExtensions[filepath.Ext(p)] {
return nil
}
p = path.Join(*distDir, p)
log.Printf("Pre-compressing %v\n", p)
eg.Go(func() error {
return precompress(p)
})
return nil
})
if err != nil {
return err
}
return eg.Wait()
}
var compressibleExtensions = map[string]bool{
".js": true,
".css": true,
".wasm": true,
}
func precompress(path string) error {
contents, err := os.ReadFile(path)
if err != nil {
return err
}
fi, err := os.Lstat(path)
if err != nil {
return err
}
err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
return gzip.NewWriterLevel(w, gzip.BestCompression)
}, path+".gz", fi.Mode())
if err != nil {
return err
}
return writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) {
return brotli.NewWriterLevel(w, brotli.BestCompression), nil
}, path+".br", fi.Mode())
}
func writeCompressed(contents []byte, compressedWriterCreator func(io.Writer) (io.WriteCloser, error), outputPath string, outputMode fs.FileMode) error {
var buf bytes.Buffer
compressedWriter, err := compressedWriterCreator(&buf)
if err != nil {
return err
}
if _, err := compressedWriter.Write(contents); err != nil {
return err
}
if err := compressedWriter.Close(); err != nil {
return err
}
return os.WriteFile(outputPath, buf.Bytes(), outputMode)
}

151
cmd/tsconnect/common.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"time"
esbuild "github.com/evanw/esbuild/pkg/api"
)
const (
devMode = true
prodMode = false
)
// commonSetup performs setup that is common to both dev and build modes.
func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
// Change cwd to to where this file lives -- that's where all inputs for
// esbuild and other build steps live.
if _, filename, _, ok := runtime.Caller(0); ok {
if err := os.Chdir(path.Dir(filename)); err != nil {
return nil, fmt.Errorf("Cannot change cwd: %w", err)
}
}
if err := buildDeps(dev); err != nil {
return nil, fmt.Errorf("Cannot build deps: %w", err)
}
return &esbuild.BuildOptions{
EntryPoints: []string{"src/index.ts", "src/index.css"},
Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
Outdir: *distDir,
Bundle: true,
Sourcemap: esbuild.SourceMapLinked,
LogLevel: esbuild.LogLevelInfo,
Define: map[string]string{"DEBUG": strconv.FormatBool(dev)},
Target: esbuild.ES2017,
Plugins: []esbuild.Plugin{{
Name: "tailscale-tailwind",
Setup: func(build esbuild.PluginBuild) {
setupEsbuildTailwind(build, dev)
},
}},
}, nil
}
// buildDeps builds the static assets that are needed for the server (except for
// JS/CSS bundling, which is handled by esbuild).
func buildDeps(dev bool) error {
if err := copyWasmExec(); err != nil {
return fmt.Errorf("Cannot copy wasm_exec.js: %w", err)
}
if err := buildWasm(dev); err != nil {
return fmt.Errorf("Cannot build main.wasm: %w", err)
}
if err := installJSDeps(); err != nil {
return fmt.Errorf("Cannot install JS deps: %w", err)
}
return nil
}
// copyWasmExec grabs the current wasm_exec.js runtime helper library from the
// Go toolchain.
func copyWasmExec() error {
log.Printf("Copying wasm_exec.js...\n")
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
wasmExecDstPath := filepath.Join("src", "wasm_exec.js")
contents, err := os.ReadFile(wasmExecSrcPath)
if err != nil {
return err
}
return os.WriteFile(wasmExecDstPath, contents, 0600)
}
// buildWasm builds the Tailscale wasm binary and places it where the JS can
// load it.
func buildWasm(dev bool) error {
log.Printf("Building wasm...\n")
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
if !dev {
// Omit long paths and debug symbols in release builds, to reduce the
// generated WASM binary size.
args = append(args, "-trimpath", "-ldflags", "-s -w")
}
args = append(args, "-o", "src/main.wasm", "./wasm")
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// installJSDeps installs the JavaScript dependencies specified by package.json
func installJSDeps() error {
log.Printf("Installing JS deps...\n")
return runYarn()
}
func runYarn(args ...string) error {
cmd := exec.Command(*yarnPath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// EsbuildMetadata is the subset of metadata struct (described by
// https://esbuild.github.io/api/#metafile) that we care about for mapping
// from entry points to hashed file names.
type EsbuildMetadata struct {
Outputs map[string]struct {
EntryPoint string `json:"entryPoint,omitempty"`
} `json:"outputs,omitempty"`
}
func setupEsbuildTailwind(build esbuild.PluginBuild, dev bool) {
build.OnLoad(esbuild.OnLoadOptions{
Filter: "./src/index.css$",
}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
start := time.Now()
yarnArgs := []string{"--silent", "tailwind", "-i", args.Path}
if !dev {
yarnArgs = append(yarnArgs, "--minify")
}
cmd := exec.Command(*yarnPath, yarnArgs...)
tailwindOutput, err := cmd.Output()
log.Printf("Ran tailwind in %v\n", time.Since(start))
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
log.Printf("Tailwind stderr: %s", exitErr.Stderr)
}
return esbuild.OnLoadResult{}, fmt.Errorf("Cannot run tailwind: %w", err)
}
tailwindOutputStr := string(tailwindOutput)
return esbuild.OnLoadResult{
Contents: &tailwindOutputStr,
Loader: esbuild.LoaderCSS,
}, nil
})
}

38
cmd/tsconnect/dev.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"log"
"net"
"strconv"
esbuild "github.com/evanw/esbuild/pkg/api"
)
func runDev() {
buildOptions, err := commonSetup(devMode)
if err != nil {
log.Fatalf("Cannot setup: %v", err)
}
host, portStr, err := net.SplitHostPort(*addr)
if err != nil {
log.Fatalf("Cannot parse addr: %v", err)
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
log.Fatalf("Cannot parse port: %v", err)
}
result, err := esbuild.Serve(esbuild.ServeOptions{
Port: uint16(port),
Host: host,
Servedir: "./",
}, *buildOptions)
if err != nil {
log.Fatalf("Cannot start esbuild server: %v", err)
}
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
result.Wait()
}

2
cmd/tsconnect/dist/placeholder vendored Normal file
View File

@@ -0,0 +1,2 @@
This is here to make sure the dist/ directory exists for the go:embed command
in serve.go.

38
cmd/tsconnect/index.html Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="dist/index.css" />
</head>
<body class="flex flex-col min-h-screen">
<div class="bg-gray-100 border-b border-gray-200 pt-4 pb-2 mb-6">
<header class="container mx-auto px-4 flex flex-row items-center">
<h1 class="text-3xl font-bold grow">Tailscale Connect</h1>
<div class="text-gray-600" id="state">Loading…</div>
</header>
</div>
<form
id="ssh-form"
class="container mx-auto px-4 hidden flex justify-center"
>
<input type="text" class="input username" placeholder="Username" />
<div class="select-with-arrow mx-2">
<select class="select"></select>
</div>
<input
type="submit"
class="button bg-green-500 border-green-500 text-white hover:bg-green-600 hover:border-green-600"
value="SSH"
/>
</form>
<div id="no-ssh" class="container mx-auto px-4 hidden text-center">
None of your machines have
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link"
>Tailscale SSH</a
>
enabled. Give it a try!
</div>
<script src="dist/index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"name": "tsconnect",
"version": "0.0.1",
"license": "BSD-3-Clause",
"devDependencies": {
"@types/golang-wasm-exec": "^1.15.0",
"@types/qrcode": "^1.4.2",
"qrcode": "^1.5.0",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4",
"xterm": "^4.18.0"
},
"scripts": {
"lint": "tsc --noEmit"
},
"prettier": {
"semi": false,
"printWidth": 80
}
}

148
cmd/tsconnect/serve.go Normal file
View File

@@ -0,0 +1,148 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"time"
"tailscale.com/tsweb"
)
//go:embed index.html
var embeddedFS embed.FS
//go:embed dist/*
var embeddedDistFS embed.FS
var serveStartTime = time.Now()
func runServe() {
mux := http.NewServeMux()
var distFS fs.FS
if *distDir == "./dist" {
var err error
distFS, err = fs.Sub(embeddedDistFS, "dist")
if err != nil {
log.Fatalf("Could not drop dist/ prefix from embedded FS: %v", err)
}
} else {
distFS = os.DirFS(*distDir)
}
indexBytes, err := generateServeIndex(distFS)
if err != nil {
log.Fatalf("Could not generate index.html: %v", err)
}
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "index.html", serveStartTime, bytes.NewReader(indexBytes))
}))
mux.Handle("/dist/", http.StripPrefix("/dist/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleServeDist(w, r, distFS)
})))
tsweb.Debugger(mux)
log.Printf("Listening on %s", *addr)
err = http.ListenAndServe(*addr, mux)
if err != nil {
log.Fatal(err)
}
}
func generateServeIndex(distFS fs.FS) ([]byte, error) {
log.Printf("Generating index.html...\n")
rawIndexBytes, err := embeddedFS.ReadFile("index.html")
if err != nil {
return nil, fmt.Errorf("Could not read index.html: %w", err)
}
esbuildMetadataFile, err := distFS.Open("esbuild-metadata.json")
if err != nil {
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
}
defer esbuildMetadataFile.Close()
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
if err != nil {
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
}
var esbuildMetadata EsbuildMetadata
if err := json.Unmarshal(esbuildMetadataBytes, &esbuildMetadata); err != nil {
return nil, fmt.Errorf("Could not parse esbuild-metadata.json: %w", err)
}
entryPointsToHashedDistPaths := make(map[string]string)
for outputPath, output := range esbuildMetadata.Outputs {
if output.EntryPoint != "" {
entryPointsToHashedDistPaths[output.EntryPoint] = path.Join("dist", outputPath)
}
}
indexBytes := rawIndexBytes
for entryPointPath, defaultDistPath := range entryPointsToDefaultDistPaths {
hashedDistPath := entryPointsToHashedDistPaths[entryPointPath]
if hashedDistPath != "" {
indexBytes = bytes.ReplaceAll(indexBytes, []byte(defaultDistPath), []byte(hashedDistPath))
}
}
return indexBytes, nil
}
var entryPointsToDefaultDistPaths = map[string]string{
"src/index.css": "dist/index.css",
"src/index.ts": "dist/index.js",
}
func handleServeDist(w http.ResponseWriter, r *http.Request, distFS fs.FS) {
path := r.URL.Path
var f fs.File
// Prefer pre-compressed versions generated during the build step.
if tsweb.AcceptsEncoding(r, "br") {
if brotliFile, err := distFS.Open(path + ".br"); err == nil {
f = brotliFile
w.Header().Set("Content-Encoding", "br")
}
}
if f == nil && tsweb.AcceptsEncoding(r, "gzip") {
if gzipFile, err := distFS.Open(path + ".gz"); err == nil {
f = gzipFile
w.Header().Set("Content-Encoding", "gzip")
}
}
if f == nil {
if rawFile, err := distFS.Open(path); err == nil {
f = rawFile
} else {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
}
defer f.Close()
// fs.File does not claim to implement Seeker, but in practice it does.
fSeeker, ok := f.(io.ReadSeeker)
if !ok {
http.Error(w, "Not seekable", http.StatusInternalServerError)
return
}
// Aggressively cache static assets, since we cache-bust our assets with
// hashed filenames.
w.Header().Set("Cache-Control", "public, max-age=31535996")
w.Header().Set("Vary", "Accept-Encoding")
http.ServeContent(w, r, path, serveStartTime, fSeeker)
}

15
cmd/tsconnect/src/esbuild.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
// 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.
/**
* @fileoverview Type definitions for types generated by the esbuild build
* process.
*/
declare module "*.wasm" {
const path: string
export default path
}
declare const DEBUG: boolean

View File

@@ -0,0 +1,75 @@
/* 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. */
@import "xterm/css/xterm.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
.link {
@apply text-blue-600;
}
.link:hover {
@apply underline;
}
.button {
@apply font-medium py-1 px-2 rounded-md border border-transparent text-center cursor-pointer;
transition-property: background-color, border-color, color, box-shadow;
transition-duration: 120ms;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
min-width: 80px;
}
.button:focus {
@apply outline-none ring;
}
.button:disabled {
@apply pointer-events-none select-none;
}
.input {
@apply appearance-none leading-tight rounded-md bg-white border border-gray-300 hover:border-gray-400 transition-colors px-3;
height: 2.375rem;
}
.input::placeholder {
@apply text-gray-400;
}
.input:disabled {
@apply border-gray-200;
@apply bg-gray-50;
@apply cursor-not-allowed;
}
.input:focus {
@apply outline-none ring border-transparent;
}
.select {
@apply appearance-none py-2 px-3 leading-tight rounded-md bg-white border border-gray-300;
}
.select-with-arrow {
@apply relative;
}
.select-with-arrow .select {
width: 100%;
}
.select-with-arrow::after {
@apply absolute;
content: "";
top: 50%;
right: 0.5rem;
transform: translate(-0.3em, -0.15em);
width: 0.6em;
height: 0.4em;
opacity: 0.6;
background-color: currentColor;
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
}

View File

@@ -0,0 +1,54 @@
// 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.
import "./wasm_exec"
import wasmUrl from "./main.wasm"
import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier"
import { sessionStateStorage } from "./js-state-store"
const go = new Go()
WebAssembly.instantiateStreaming(
fetch(`./dist/${wasmUrl}`),
go.importObject
).then((result) => {
// The Go process should never exit, if it does then it's an unhandled panic.
go.run(result.instance).then(() => handleGoPanic())
const ipn = newIPN({
// Persist IPN state in sessionStorage in development, so that we don't need
// to re-authorize every time we reload the page.
stateStorage: DEBUG ? sessionStateStorage : undefined,
})
ipn.run({
notifyState: notifyState.bind(null, ipn),
notifyNetMap: notifyNetMap.bind(null, ipn),
notifyBrowseToURL: notifyBrowseToURL.bind(null, ipn),
notifyPanicRecover: handleGoPanic,
})
})
function handleGoPanic(err?: string) {
if (DEBUG && err) {
console.error("Go panic", err)
}
if (panicNode) {
panicNode.remove()
}
panicNode = document.createElement("div")
panicNode.className =
"rounded bg-red-500 p-2 absolute top-2 right-2 text-white font-bold text-right cursor-pointer"
panicNode.textContent = "Tailscale has encountered an error."
const panicDetailNode = document.createElement("div")
panicDetailNode.className = "text-sm font-normal"
panicDetailNode.textContent = "Click to reload"
panicNode.appendChild(panicDetailNode)
panicNode.addEventListener("click", () => location.reload(), {
once: true,
})
document.body.appendChild(panicNode)
setTimeout(() => {
panicNode!.remove()
}, 10000)
}
let panicNode: HTMLDivElement | undefined

View File

@@ -0,0 +1,14 @@
// 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.
/** @fileoverview Callbacks used by jsStateStore to persist IPN state. */
export const sessionStateStorage: IPNStateStorage = {
setState(id, value) {
window.sessionStorage[`ipn-state-${id}`] = value
},
getState(id) {
return window.sessionStorage[`ipn-state-${id}`] || ""
},
}

View File

@@ -0,0 +1,73 @@
// 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.
import * as qrcode from "qrcode"
export async function showLoginURL(url: string) {
if (loginNode) {
loginNode.remove()
}
loginNode = document.createElement("div")
loginNode.className = "flex flex-col items-center justify-items-center"
const linkNode = document.createElement("a")
linkNode.className = "link"
linkNode.href = url
linkNode.target = "_blank"
loginNode.appendChild(linkNode)
try {
const dataURL = await qrcode.toDataURL(url, { width: 512 })
const imageNode = document.createElement("img")
imageNode.className = "mx-auto"
imageNode.src = dataURL
imageNode.width = 256
imageNode.height = 256
linkNode.appendChild(imageNode)
} catch (err) {
console.error("Could not generate QR code:", err)
}
linkNode.appendChild(document.createTextNode(url))
document.body.appendChild(loginNode)
}
export function hideLoginURL() {
if (!loginNode) {
return
}
loginNode.remove()
loginNode = undefined
}
let loginNode: HTMLDivElement | undefined
export function showLogoutButton(ipn: IPN) {
if (logoutButtonNode) {
logoutButtonNode.remove()
}
logoutButtonNode = document.createElement("button")
logoutButtonNode.className =
"button bg-gray-500 border-gray-500 text-white hover:bg-gray-600 hover:border-gray-600 ml-2 font-bold"
logoutButtonNode.textContent = "Logout"
logoutButtonNode.addEventListener(
"click",
() => {
ipn.logout()
},
{ once: true }
)
const headerNode = document.getElementsByTagName("header")[0]!
headerNode.appendChild(logoutButtonNode)
}
export function hideLogoutButton() {
if (!logoutButtonNode) {
return
}
logoutButtonNode.remove()
logoutButtonNode = undefined
}
let logoutButtonNode: HTMLButtonElement | undefined

View File

@@ -0,0 +1,65 @@
// 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.
import {
showLoginURL,
hideLoginURL,
showLogoutButton,
hideLogoutButton,
} from "./login"
import { showSSHForm, hideSSHForm } from "./ssh"
import { IPNState } from "./wasm_js"
/**
* @fileoverview Notification callback functions (bridged from ipn.Notify)
*/
export function notifyState(ipn: IPN, state: IPNState) {
let stateLabel
switch (state) {
case IPNState.NoState:
stateLabel = "Initializing…"
break
case IPNState.InUseOtherUser:
stateLabel = "In-use by another user"
break
case IPNState.NeedsLogin:
stateLabel = "Needs Login"
hideLogoutButton()
hideSSHForm()
ipn.login()
break
case IPNState.NeedsMachineAuth:
stateLabel = "Needs authorization"
break
case IPNState.Stopped:
stateLabel = "Stopped"
hideLogoutButton()
hideSSHForm()
break
case IPNState.Starting:
stateLabel = "Starting…"
break
case IPNState.Running:
stateLabel = "Running"
hideLoginURL()
showLogoutButton(ipn)
break
}
const stateNode = document.getElementById("state") as HTMLDivElement
stateNode.textContent = stateLabel ?? ""
}
export function notifyNetMap(ipn: IPN, netMapStr: string) {
const netMap = JSON.parse(netMapStr) as IPNNetMap
if (DEBUG) {
console.log("Received net map: " + JSON.stringify(netMap, null, 2))
}
showSSHForm(netMap.peers, ipn)
}
export function notifyBrowseToURL(ipn: IPN, url: string) {
showLoginURL(url)
}

80
cmd/tsconnect/src/ssh.ts Normal file
View File

@@ -0,0 +1,80 @@
// 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.
import { Terminal } from "xterm"
export function showSSHForm(peers: IPNNetMapPeerNode[], ipn: IPN) {
const formNode = document.getElementById("ssh-form") as HTMLDivElement
const noSSHNode = document.getElementById("no-ssh") as HTMLDivElement
const sshPeers = peers.filter(
(p) => p.tailscaleSSHEnabled && p.online !== false
)
if (sshPeers.length == 0) {
formNode.classList.add("hidden")
noSSHNode.classList.remove("hidden")
return
}
sshPeers.sort((a, b) => a.name.localeCompare(b.name))
const selectNode = formNode.querySelector("select")!
selectNode.innerHTML = ""
for (const p of sshPeers) {
const option = document.createElement("option")
option.textContent = p.name.split(".")[0]
option.value = p.name
selectNode.appendChild(option)
}
const usernameNode = formNode.querySelector(".username") as HTMLInputElement
formNode.onsubmit = (e) => {
e.preventDefault()
const hostname = selectNode.value
ssh(hostname, usernameNode.value, ipn)
}
noSSHNode.classList.add("hidden")
formNode.classList.remove("hidden")
}
export function hideSSHForm() {
const formNode = document.getElementById("ssh-form") as HTMLDivElement
formNode.classList.add("hidden")
}
function ssh(hostname: string, username: string, ipn: IPN) {
const termContainerNode = document.createElement("div")
termContainerNode.className = "p-3"
document.body.appendChild(termContainerNode)
const term = new Terminal({
cursorBlink: true,
})
term.open(termContainerNode)
// Cancel wheel events from scrolling the page if the terminal has scrollback
termContainerNode.addEventListener("wheel", (e) => {
if (term.buffer.active.baseY > 0) {
e.preventDefault()
}
})
let onDataHook: ((data: string) => void) | undefined
term.onData((e) => {
onDataHook?.(e)
})
term.focus()
ipn.ssh(hostname, username, {
writeFn: (input) => term.write(input),
setReadFn: (hook) => (onDataHook = hook),
rows: term.rows,
cols: term.cols,
onDone: () => {
term.dispose()
termContainerNode.remove()
},
})
}

View File

@@ -0,0 +1,86 @@
// 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.
/**
* @fileoverview Type definitions for types exported by the wasm_js.go Go
* module. Not actually a .d.ts file so that we can use enums from it in
* esbuild's simplified TypeScript compiler (see https://github.com/evanw/esbuild/issues/2298#issuecomment-1146378367)
*/
declare global {
function newIPN(config: IPNConfig): IPN
interface IPN {
run(callbacks: IPNCallbacks): void
login(): void
logout(): void
ssh(
host: string,
username: string,
termConfig: {
writeFn: (data: string) => void
setReadFn: (readFn: (data: string) => void) => void
rows: number
cols: number
onDone: () => void
}
): void
}
interface IPNStateStorage {
setState(id: string, value: string): void
getState(id: string): string
}
type IPNConfig = {
stateStorage?: IPNStateStorage
}
type IPNCallbacks = {
notifyState: (state: IPNState) => void
notifyNetMap: (netMapStr: string) => void
notifyBrowseToURL: (url: string) => void
notifyPanicRecover: (err: string) => void
}
type IPNNetMap = {
self: IPNNetMapSelfNode
peers: IPNNetMapPeerNode[]
}
type IPNNetMapNode = {
name: string
addresses: string[]
machineKey: string
nodeKey: string
}
type IPNNetMapSelfNode = IPNNetMapNode & {
machineStatus: IPNMachineStatus
}
type IPNNetMapPeerNode = IPNNetMapNode & {
online?: boolean
tailscaleSSHEnabled: boolean
}
}
/** Mirrors values from ipn/backend.go */
export const enum IPNState {
NoState = 0,
InUseOtherUser = 1,
NeedsLogin = 2,
NeedsMachineAuth = 3,
Stopped = 4,
Starting = 5,
Running = 6,
}
/** Mirrors values from MachineStatus in tailcfg.go */
export const enum IPNMachineStatus {
MachineUnknown = 0,
MachineUnauthorized = 1,
MachineAuthorized = 2,
MachineInvalid = 3,
}

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.ts"],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "ES2020",
"moduleResolution": "node",
"isolatedModules": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

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.
// The tsconnect command builds and serves the static site that is generated for
// the Tailscale Connect JS/WASM client. Can be run in 3 modes:
// - dev: builds the site and serves it. JS and CSS changes can be picked up
// with a reload.
// - build: builds the site and writes it to dist/
// - serve: serves the site from dist/ (embedded in the binary)
package main // import "tailscale.com/cmd/tsconnect"
import (
"flag"
"fmt"
"log"
"os"
)
var (
addr = flag.String("addr", ":9090", "address to listen on")
distDir = flag.String("distdir", "./dist", "path of directory to place build output in")
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
)
func main() {
flag.Usage = usage
flag.Parse()
if len(flag.Args()) != 1 {
flag.Usage()
}
switch flag.Arg(0) {
case "dev":
runDev()
case "build":
runBuild()
case "serve":
runServe()
default:
log.Printf("Unknown command: %s", flag.Arg(0))
flag.Usage()
}
}
func usage() {
fmt.Fprintf(os.Stderr, `
usage: tsconnect {dev|build|serve}
`[1:])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, `
tsconnect implements development/build/serving workflows for Tailscale Connect.
It can be invoked with one of three subcommands:
- dev: Run in development mode, allowing JS and CSS changes to be picked up without a rebuilt or restart.
- build: Run in production build mode (generating static assets)
- serve: Run in production serve mode (serving static assets)
`[1:])
os.Exit(2)
}

View File

@@ -0,0 +1,430 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The wasm package builds a WebAssembly module that provides a subset of
// Tailscale APIs to JavaScript.
//
// When run in the browser, a newIPN(config) function is added to the global JS
// namespace. When called it returns an ipn object with the methods
// run(callbacks), login(), logout(), and ssh(...).
package main
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"math/rand"
"net"
"net/netip"
"strings"
"syscall/js"
"time"
"golang.org/x/crypto/ssh"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/words"
)
func main() {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal("Usage: newIPN(config)")
return nil
}
return newIPN(args[0])
}))
// Keep Go runtime alive, otherwise it will be shut down before newIPN gets
// called.
<-make(chan bool)
}
func newIPN(jsConfig js.Value) map[string]any {
netns.SetEnabled(false)
var logf logger.Logf = log.Printf
dialer := new(tsdial.Dialer)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Dialer: dialer,
})
if err != nil {
log.Fatal(err)
}
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
ns.ProcessLocalIPs = true
ns.ProcessSubnets = true
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
return true
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
jsStateStorage := jsConfig.Get("stateStorage")
var store ipn.StateStore
if jsStateStorage.IsUndefined() {
store = new(mem.Store)
} else {
store = &jsStateStore{jsStateStorage}
}
srv, err := ipnserver.New(log.Printf, "some-logid", store, eng, dialer, nil, ipnserver.Options{
SurviveDisconnects: true,
LoginFlags: controlclient.LoginEphemeral,
})
if err != nil {
log.Fatalf("ipnserver.New: %v", err)
}
lb := srv.LocalBackend()
jsIPN := &jsIPN{
dialer: dialer,
srv: srv,
lb: lb,
}
return map[string]any{
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal(`Usage: run({
notifyState(state: int): void,
notifyNetMap(netMap: object): void,
notifyBrowseToURL(url: string): void,
notifyPanicRecover(err: string): void,
})`)
return nil
}
jsIPN.run(args[0])
return nil
}),
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: login()")
return nil
}
jsIPN.login()
return nil
}),
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: logout()")
return nil
}
jsIPN.logout()
return nil
}),
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 3 {
log.Printf("Usage: ssh(hostname, userName, termConfig)")
return nil
}
go jsIPN.ssh(
args[0].String(),
args[1].String(),
args[2])
return nil
}),
}
}
type jsIPN struct {
dialer *tsdial.Dialer
srv *ipnserver.Server
lb *ipnlocal.LocalBackend
}
func (i *jsIPN) run(jsCallbacks js.Value) {
notifyState := func(state ipn.State) {
jsCallbacks.Call("notifyState", int(state))
}
notifyState(ipn.NoState)
i.lb.SetNotifyCallback(func(n ipn.Notify) {
// Panics in the notify callback are likely due to be due to bugs in
// this bridging module (as opposed to actual bugs in Tailscale) and
// thus may be recoverable. Let the UI know, and allow the user to
// choose if they want to reload the page.
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic recovered:", r)
jsCallbacks.Call("notifyPanicRecover", fmt.Sprint(r))
}
}()
log.Printf("NOTIFY: %+v", n)
if n.State != nil {
notifyState(*n.State)
}
if nm := n.NetMap; nm != nil && i.lb.State() == ipn.Running {
jsNetMap := jsNetMap{
Self: jsNetMapSelfNode{
jsNetMapNode: jsNetMapNode{
Name: nm.Name,
Addresses: mapSlice(nm.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
NodeKey: nm.NodeKey.String(),
MachineKey: nm.MachineKey.String(),
},
MachineStatus: int(nm.MachineStatus),
},
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode {
name := p.Name
if name == "" {
// In practice this should only happen for Hello.
name = p.Hostinfo.Hostname()
}
return jsNetMapPeerNode{
jsNetMapNode: jsNetMapNode{
Name: name,
Addresses: mapSlice(p.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
MachineKey: p.Machine.String(),
NodeKey: p.Key.String(),
},
Online: p.Online,
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
}
}),
}
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
} else {
log.Printf("Could not generate JSON netmap: %v", err)
}
}
if n.BrowseToURL != nil {
jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
}
})
go func() {
err := i.lb.Start(ipn.Options{
StateKey: "wasm",
UpdatePrefs: &ipn.Prefs{
ControlURL: ipn.DefaultControlURL,
RouteAll: false,
AllowSingleHosts: true,
WantRunning: true,
Hostname: generateHostname(),
},
})
if err != nil {
log.Printf("Start error: %v", err)
}
}()
go func() {
ln, _, err := safesocket.Listen("", 0)
if err != nil {
log.Fatalf("safesocket.Listen: %v", err)
}
err = i.srv.Run(context.Background(), ln)
log.Fatalf("ipnserver.Run exited: %v", err)
}()
}
func (i *jsIPN) login() {
go i.lb.StartLoginInteractive()
}
func (i *jsIPN) logout() {
if i.lb.State() == ipn.NoState {
log.Printf("Backend not running")
}
go i.lb.Logout()
}
func (i *jsIPN) ssh(host, username string, termConfig js.Value) {
writeFn := termConfig.Get("writeFn")
setReadFn := termConfig.Get("setReadFn")
rows := termConfig.Get("rows").Int()
cols := termConfig.Get("cols").Int()
onDone := termConfig.Get("onDone")
defer onDone.Invoke()
write := func(s string) {
writeFn.Invoke(s)
}
writeError := func(label string, err error) {
write(fmt.Sprintf("%s Error: %v\r\n", label, err))
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c, err := i.dialer.UserDial(ctx, "tcp", net.JoinHostPort(host, "22"))
if err != nil {
writeError("Dial", err)
return
}
defer c.Close()
config := &ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
User: username,
}
sshConn, _, _, err := ssh.NewClientConn(c, host, config)
if err != nil {
writeError("SSH Connection", err)
return
}
defer sshConn.Close()
write("SSH Connected\r\n")
sshClient := ssh.NewClient(sshConn, nil, nil)
defer sshClient.Close()
session, err := sshClient.NewSession()
if err != nil {
writeError("SSH Session", err)
return
}
write("Session Established\r\n")
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
writeError("SSH Stdin", err)
return
}
session.Stdout = termWriter{writeFn}
session.Stderr = termWriter{writeFn}
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
input := args[0].String()
_, err := stdin.Write([]byte(input))
if err != nil {
writeError("Write Input", err)
}
return nil
}))
err = session.RequestPty("xterm", rows, cols, ssh.TerminalModes{})
if err != nil {
writeError("Pseudo Terminal", err)
return
}
err = session.Shell()
if err != nil {
writeError("Shell", err)
return
}
err = session.Wait()
if err != nil {
writeError("Exit", err)
return
}
}
type termWriter struct {
f js.Value
}
func (w termWriter) Write(p []byte) (n int, err error) {
r := bytes.Replace(p, []byte("\n"), []byte("\n\r"), -1)
w.f.Invoke(string(r))
return len(p), nil
}
type jsNetMap struct {
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`
}
type jsNetMapNode struct {
Name string `json:"name"`
Addresses []string `json:"addresses"`
MachineKey string `json:"machineKey"`
NodeKey string `json:"nodeKey"`
}
type jsNetMapSelfNode struct {
jsNetMapNode
MachineStatus int `json:"machineStatus"`
}
type jsNetMapPeerNode struct {
jsNetMapNode
Online *bool `json:"online,omitempty"`
TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
}
type jsStateStore struct {
jsStateStorage js.Value
}
func (s *jsStateStore) ReadState(id ipn.StateKey) ([]byte, error) {
jsValue := s.jsStateStorage.Call("getState", string(id))
if jsValue.String() == "" {
return nil, ipn.ErrStateNotExist
}
return hex.DecodeString(jsValue.String())
}
func (s *jsStateStore) WriteState(id ipn.StateKey, bs []byte) error {
s.jsStateStorage.Call("setState", string(id), hex.EncodeToString(bs))
return nil
}
func mapSlice[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func filterSlice[T any](a []T, f func(T) bool) []T {
n := make([]T, 0, len(a))
for _, e := range a {
if f(e) {
n = append(n, e)
}
}
return n
}
func generateHostname() string {
tails := words.Tails()
scales := words.Scales()
if rand.Int()%2 == 0 {
// JavaScript
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "j") })
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "s") })
} else {
// WebAssembly
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "w") })
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "a") })
}
tail := tails[rand.Intn(len(tails))]
scale := scales[rand.Intn(len(scales))]
return fmt.Sprintf("%s-%s", tail, scale)
}

644
cmd/tsconnect/yarn.lock Normal file
View File

@@ -0,0 +1,644 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/golang-wasm-exec@^1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@types/golang-wasm-exec/-/golang-wasm-exec-1.15.0.tgz#d0aafbb2b0dc07eaf45dfb83bfb6cdd5b2b3c55c"
integrity sha512-FrL97mp7WW8LqNinVkzTVKOIQKuYjQqgucnh41+1vRQ+bf1LT8uh++KRf9otZPXsa6H1p8ruIGz1BmCGttOL6Q==
"@types/node@*":
version "18.6.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.1.tgz#828e4785ccca13f44e2fb6852ae0ef11e3e20ba5"
integrity sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==
"@types/qrcode@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.4.2.tgz#7d7142d6fa9921f195db342ed08b539181546c74"
integrity sha512-7uNT9L4WQTNJejHTSTdaJhfBSCN73xtXaHFyBJ8TSwiLhe4PRuTue7Iph0s2nG9R/ifUaSnGhLUOZavlBEqDWQ==
dependencies:
"@types/node" "*"
acorn-node@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
dependencies:
acorn "^7.0.0"
acorn-walk "^7.0.0"
xtend "^4.0.2"
acorn-walk@^7.0.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn@^7.0.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@^1.1.4, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
detective@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
dependencies:
acorn-node "^1.8.2"
defined "^1.0.0"
minimist "^1.2.6"
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
dijkstrajs@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257"
integrity sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encode-utf8@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
fast-glob@^3.2.11:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
dependencies:
reusify "^1.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies:
is-glob "^4.0.3"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
lilconfig@^2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.2"
picomatch "^2.3.1"
minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
postcss-import@^14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-js@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
dependencies:
lilconfig "^2.0.5"
yaml "^1.10.2"
postcss-nested@5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==
dependencies:
postcss-selector-parser "^6.0.6"
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.14:
version "8.4.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
dependencies:
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^1.0.2"
qrcode@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b"
integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==
dependencies:
dijkstrajs "^1.0.1"
encode-utf8 "^1.0.3"
pngjs "^5.0.0"
yargs "^15.3.1"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
dependencies:
pify "^2.3.0"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
resolve@^1.1.7, resolve@^1.22.1:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwindcss@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.6.tgz#bcb719357776c39e6376a8d84e9834b2b19a49f1"
integrity sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==
dependencies:
arg "^5.0.2"
chokidar "^3.5.3"
color-name "^1.1.4"
detective "^5.2.1"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.11"
glob-parent "^6.0.2"
is-glob "^4.0.3"
lilconfig "^2.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.14"
postcss-import "^14.1.0"
postcss-js "^4.0.0"
postcss-load-config "^3.1.4"
postcss-nested "5.0.6"
postcss-selector-parser "^6.0.10"
postcss-value-parser "^4.2.0"
quick-lru "^5.1.1"
resolve "^1.22.1"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
typescript@^4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm@^4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0.tgz#a1f6ab2c330c3918fb094ae5f4c2562987398ea1"
integrity sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yaml@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"

View File

@@ -2,177 +2,13 @@
// 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.
// See https://github.com/tailscale/tailscale/issues/3802
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"syscall"
"time"
"unsafe"
"github.com/creack/pty"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/tempfork/gliderlabs/ssh"
)
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})))
}

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")
}

View File

@@ -7,19 +7,29 @@ package tests
import (
"fmt"
"inet.af/netaddr"
"net/netip"
)
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices
type StructWithoutPtrs struct {
Int int
Pfx netaddr.IPPrefix
Pfx netip.Prefix
}
type Map struct {
M map[string]int
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 {
@@ -43,6 +53,6 @@ type StructWithSlices struct {
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Prefixes []netip.Prefix
Data []byte
}

View File

@@ -7,7 +7,7 @@
package tests
import (
"inet.af/netaddr"
"net/netip"
)
// Clone makes a deep copy of StructWithPtrs.
@@ -50,7 +50,7 @@ func (src *StructWithoutPtrs) Clone() *StructWithoutPtrs {
// 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
Pfx netip.Prefix
}{})
// Clone makes a deep copy of Map.
@@ -61,10 +61,64 @@ func (src *Map) Clone() *Map {
}
dst := new(Map)
*dst = *src
if dst.M != nil {
dst.M = map[string]int{}
for k, v := range src.M {
dst.M[k] = v
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
@@ -72,7 +126,16 @@ func (src *Map) Clone() *Map {
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _MapCloneNeedsRegeneration = Map(struct {
M map[string]int
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.
@@ -115,6 +178,6 @@ var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
Structs []StructWithPtrs
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Prefixes []netip.Prefix
Data []byte
}{})

View File

@@ -9,9 +9,9 @@ package tests
import (
"encoding/json"
"errors"
"net/netip"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/types/views"
)
@@ -134,13 +134,13 @@ func (v *StructWithoutPtrsView) UnmarshalJSON(b []byte) error {
return nil
}
func (v StructWithoutPtrsView) Int() int { return v.ж.Int }
func (v StructWithoutPtrsView) Pfx() netaddr.IPPrefix { return v.ж.Pfx }
func (v StructWithoutPtrsView) Int() int { return v.ж.Int }
func (v StructWithoutPtrsView) Pfx() netip.Prefix { 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
Pfx netip.Prefix
}{})
// View returns a readonly view of Map.
@@ -188,9 +188,57 @@ func (v *MapView) UnmarshalJSON(b []byte) error {
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 {
M map[string]int
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.
@@ -263,6 +311,6 @@ var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
Structs []StructWithPtrs
Ints []*int
Slice []string
Prefixes []netaddr.IPPrefix
Prefixes []netip.Prefix
Data []byte
}{})

View File

@@ -88,8 +88,12 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
{{end}}
{{define "mapField"}}
// Unsupported, panics.
func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
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}}
@@ -132,6 +136,11 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
FieldName string
FieldType string
FieldViewName string
MapKeyType string
MapValueType string
MapValueView string
MapFn string
}{
StructName: typ.Obj().Name(),
ViewName: typ.Obj().Name() + "View",
@@ -168,7 +177,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
case "byte":
it.Import("go4.org/mem")
writeTemplate("byteSliceField")
case "inet.af/netaddr.IPPrefix":
case "inet.af/netip.Prefix", "net/netip.Prefix":
it.Import("tailscale.com/types/views")
writeTemplate("ipPrefixSliceField")
default:
@@ -194,7 +203,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
writeTemplate("sliceField")
}
continue
case *types.Struct:
case *types.Struct, *types.Named:
strucT := underlying
args.FieldType = it.QualifiedName(fieldType)
if codegen.ContainsPointers(strucT) {
@@ -204,9 +213,62 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
writeTemplate("valueField")
continue
case *types.Map:
// TODO(maisem): support this.
// args.FieldType = importedName(ft)
// writeTemplate("mapField")
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

View File

@@ -6,6 +6,7 @@ package controlclient
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
@@ -41,20 +42,22 @@ func (g *LoginGoal) sendLogoutError(err error) {
}
}
var _ Client = (*Auto)(nil)
// Auto connects to a tailcontrol server for a node.
// It's a concrete implementation of the Client interface.
type Auto struct {
direct *Direct // our interface to the server APIs
timeNow func() time.Time
logf logger.Logf
expiry *time.Time
closed bool
newMapCh chan struct{} // readable when we must restart a map request
direct *Direct // our interface to the server APIs
timeNow func() time.Time
logf logger.Logf
expiry *time.Time
closed bool
newMapCh chan struct{} // readable when we must restart a map request
statusFunc func(Status) // called to update Client status; always non-nil
unregisterHealthWatch func()
mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status
mu sync.Mutex // mutex guards the following fields
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
@@ -90,6 +93,9 @@ func NewNoStart(opts Options) (*Auto, error) {
if err != nil {
return nil, err
}
if opts.Status == nil {
return nil, errors.New("missing required Options.Status")
}
if opts.Logf == nil {
opts.Logf = func(fmt string, args ...any) {}
}
@@ -97,13 +103,14 @@ func NewNoStart(opts Options) (*Auto, error) {
opts.TimeNow = time.Now
}
c := &Auto{
direct: direct,
timeNow: opts.TimeNow,
logf: opts.Logf,
newMapCh: make(chan struct{}, 1),
quit: make(chan struct{}),
authDone: make(chan struct{}),
mapDone: make(chan struct{}),
direct: direct,
timeNow: opts.TimeNow,
logf: opts.Logf,
newMapCh: make(chan struct{}, 1),
quit: make(chan struct{}),
authDone: make(chan struct{}),
mapDone: make(chan struct{}),
statusFunc: opts.Status,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
@@ -289,6 +296,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.")
@@ -296,6 +304,7 @@ func (c *Auto) authRoutine() {
}
if !goal.wantLoggedIn {
health.SetAuthRoutineInError(nil)
err := c.direct.TryLogout(ctx)
goal.sendLogoutError(err)
if err != nil {
@@ -334,6 +343,7 @@ func (c *Auto) authRoutine() {
f = "TryLogin"
}
if err != nil {
health.SetAuthRoutineInError(err)
report(err, f)
bo.BackOff(ctx, err)
continue
@@ -358,6 +368,7 @@ func (c *Auto) authRoutine() {
}
// success
health.SetAuthRoutineInError(nil)
c.mu.Lock()
c.loggedIn = true
c.loginGoal = nil
@@ -458,7 +469,7 @@ func (c *Auto) mapRoutine() {
c.mu.Unlock()
health.SetInPollNetMap(false)
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
err := c.direct.PollNetMap(ctx, func(nm *netmap.NetworkMap) {
health.SetInPollNetMap(true)
c.mu.Lock()
@@ -527,13 +538,6 @@ func (c *Auto) AuthCantContinue() bool {
return !c.loggedIn && (c.loginGoal == nil || c.loginGoal.url != "")
}
// SetStatusFunc sets fn as the callback to run on any status change.
func (c *Auto) SetStatusFunc(fn func(Status)) {
c.mu.Lock()
c.statusFunc = fn
c.mu.Unlock()
}
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
if hi == nil {
panic("nil Hostinfo")
@@ -561,10 +565,13 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return
}
state := c.state
loggedIn := c.loggedIn
synced := c.synced
statusFunc := c.statusFunc
c.inSendStatus++
c.mu.Unlock()
@@ -595,9 +602,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
State: state,
Err: err,
}
if statusFunc != nil {
statusFunc(new)
}
c.statusFunc(new)
c.mu.Lock()
c.inSendStatus--
@@ -662,11 +667,8 @@ func (c *Auto) SetExpirySooner(ctx context.Context, expiry time.Time) error {
// them to the control server if they've changed.
//
// It does not retain the provided slice.
//
// The localPort field is unused except for integration tests in
// another repo.
func (c *Auto) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
changed := c.direct.SetEndpoints(localPort, endpoints)
func (c *Auto) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
changed := c.direct.SetEndpoints(endpoints)
if changed {
c.sendNewMapRequest()
}
@@ -681,7 +683,6 @@ func (c *Auto) Shutdown() {
direct := c.direct
if !closed {
c.closed = true
c.statusFunc = nil
}
c.mu.Unlock()

View File

@@ -11,8 +11,6 @@ package controlclient
import (
"context"
"net/http"
"time"
"tailscale.com/tailcfg"
)
@@ -29,9 +27,6 @@ const (
// Currently this is done through a pair of polling https requests in
// the Auto client, but that might change eventually.
type Client interface {
// SetStatusFunc provides a callback to call when control sends us
// a message.
SetStatusFunc(func(Status))
// Shutdown closes this session, which should not be used any further
// afterwards.
Shutdown()
@@ -47,9 +42,6 @@ type Client interface {
// Logout starts a synchronous logout process. It doesn't return
// until the logout operation has been completed.
Logout(context.Context) error
// SetExpirySooner sets the node's expiry time via the controlclient,
// as long as it's shorter than the current expiry time.
SetExpirySooner(context.Context, time.Time) error
// SetPaused pauses or unpauses the controlclient activity as much
// as possible, without losing its internal state, to minimize
// unnecessary network activity.
@@ -75,17 +67,10 @@ type Client interface {
SetNetInfo(*tailcfg.NetInfo)
// UpdateEndpoints changes the Endpoint structure that will be sent
// in subsequent node registration requests.
// The localPort field is unused except for integration tests in another repo.
// TODO: a server-side change would let us simply upload this
// in a separate http request. It has nothing to do with the rest of
// the state machine.
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
// SetDNS sends the SetDNSRequest request to the control plane server,
// requesting a DNS record be created or updated.
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
// DoNoiseRequest sends an HTTP request to the control plane
// over the Noise transport.
DoNoiseRequest(*http.Request) (*http.Response, error)
UpdateEndpoints(endpoints []tailcfg.Endpoint)
}
// UserVisibleError is an error that should be shown to users.

View File

@@ -16,6 +16,7 @@ import (
"io/ioutil"
"log"
"net/http"
"net/netip"
"net/url"
"os"
"reflect"
@@ -26,8 +27,6 @@ import (
"time"
"go4.org/mem"
"golang.org/x/sync/singleflight"
"inet.af/netaddr"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/health"
@@ -50,6 +49,7 @@ import (
"tailscale.com/types/persist"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/systemd"
"tailscale.com/wgengine/monitor"
)
@@ -77,7 +77,7 @@ type Direct struct {
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
serverNoiseKey key.MachinePublic
sfGroup singleflight.Group // protects noiseClient creation.
sfGroup singleflight.Group[struct{}, *noiseClient] // protects noiseClient creation.
noiseClient *noiseClient
persist persist.Persist
@@ -88,7 +88,6 @@ type Direct struct {
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
lastPingURL string // last PingRequest.URL received, for dup suppression
}
@@ -109,6 +108,9 @@ type Options struct {
PopBrowserURL func(url string) // optional func to open browser
Dialer *tsdial.Dialer // non-nil
// Status is called when there's a change in status.
Status func(Status)
// KeepSharerAndUserSplit controls whether the client
// understands Node.Sharer. If false, the Sharer is mapped to the User.
KeepSharerAndUserSplit bool
@@ -124,11 +126,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 ping with the peer handling the given IP and
// then call cb with its ping latency & method.
Ping(ip netaddr.IP, pingType tailcfg.PingType, cb func(*ipnstate.PingResult))
// Ping is a request to do a ping with the peer handling the given IP.
Ping(ctx context.Context, ip netip.Addr, pingType tailcfg.PingType) (*ipnstate.PingResult, error)
}
type Decompressor interface {
@@ -587,20 +588,19 @@ func sameEndpoints(a, b []tailcfg.Endpoint) bool {
// whether they've changed.
//
// It does not retain the provided slice.
func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
func (c *Direct) newEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
c.mu.Lock()
defer c.mu.Unlock()
// Nothing new?
if c.localPort == localPort && sameEndpoints(c.endpoints, endpoints) {
if sameEndpoints(c.endpoints, endpoints) {
return false // unchanged
}
var epStrs []string
for _, ep := range endpoints {
epStrs = append(epStrs, ep.Addr.String())
}
c.logf("[v2] client.newEndpoints(%v, %v)", localPort, epStrs)
c.localPort = localPort
c.logf("[v2] client.newEndpoints(%v)", epStrs)
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
c.everEndpoints = true
@@ -611,28 +611,37 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (c
// SetEndpoints updates the list of locally advertised endpoints.
// It won't be replicated to the server until a *fresh* call to PollNetMap().
// You don't need to restart PollNetMap if we return changed==false.
func (c *Direct) SetEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
// (no log message on function entry, because it clutters the logs
// if endpoints haven't changed. newEndpoints() will log it.)
return c.newEndpoints(localPort, endpoints)
return c.newEndpoints(endpoints)
}
func inTest() bool { return flag.Lookup("test.v") != nil }
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
//
// maxPolls is how many network maps to download; common values are 1
// or -1 (to keep a long-poll query open to the server).
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
return c.sendMapRequest(ctx, maxPolls, cb)
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
return c.sendMapRequest(ctx, -1, false, cb)
}
// FetchNetMap fetches the netmap once.
func (c *Direct) FetchNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
var ret *netmap.NetworkMap
err := c.sendMapRequest(ctx, 1, false, func(nm *netmap.NetworkMap) {
ret = nm
})
if err == nil && ret == nil {
return nil, errors.New("[unexpected] sendMapRequest success without callback")
}
return ret, err
}
// SendLiteMapUpdate makes a /map request to update the server of our latest state,
// but does not fetch anything. It returns an error if the server did not return a
// successful 200 OK response.
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
return c.sendMapRequest(ctx, 1, nil)
return c.sendMapRequest(ctx, 1, false, nil)
}
// If we go more than pollTimeout without hearing from the server,
@@ -641,7 +650,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
const pollTimeout = 120 * time.Second
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool, cb func(*netmap.NetworkMap)) error {
metricMapRequests.Add(1)
metricMapRequestsActive.Add(1)
defer metricMapRequestsActive.Add(-1)
@@ -658,7 +667,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
serverNoiseKey := c.serverNoiseKey
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
localPort := c.localPort
var epStrs []string
var epTypes []tailcfg.EndpointType
for _, ep := range c.endpoints {
@@ -684,7 +692,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
allowStream := maxPolls != 1
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, epStrs)
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
vlogf := logger.Discard
if Debug.NetMap {
@@ -704,6 +712,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
Hostinfo: hi,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
}
var extraDebugFlags []string
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
@@ -726,17 +744,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
if c.newDecompressor != nil {
request.Compress = "zstd"
}
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
if len(epStrs) == 0 && !everEndpoints && !inTest() {
request.ReadOnly = true
}
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
if err != nil {
@@ -941,14 +948,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
nm.SelfNode.Capabilities = nil
}
// Get latest localPort. This might've changed if
// a lite map update occurred meanwhile. This only affects
// the end-to-end test.
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
c.mu.Lock()
nm.LocalPort = c.localPort
c.mu.Unlock()
// Occasionally print the netmap header.
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
@@ -1168,8 +1167,8 @@ func TrimWGConfig() opt.Bool {
// It should not return false positives.
//
// TODO(bradfitz): Change controlclient.Options.SkipIPForwardingCheck into a
// func([]netaddr.IPPrefix) error signature instead.
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
// func([]netip.Prefix) error signature instead.
func ipForwardingBroken(routes []netip.Prefix, state *interfaces.State) bool {
warn, err := netutil.CheckIPForwarding(routes, state)
if err != nil {
// Oh well, we tried. This is just for debugging.
@@ -1208,10 +1207,8 @@ func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinge
}
for _, t := range strings.Split(pr.Types, ",") {
switch pt := tailcfg.PingType(t); pt {
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP:
case tailcfg.PingTSMP, tailcfg.PingDisco, tailcfg.PingICMP, tailcfg.PingPeerAPI:
go doPingerPing(logf, c, pr, pinger, pt)
// TODO(tailscale/corp#754)
// case "peerapi":
default:
logf("unsupported ping request type: %q", t)
}
@@ -1283,13 +1280,12 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
if nc != nil {
return nc, nil
}
np, err, _ := c.sfGroup.Do("noise", func() (any, error) {
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
k, err := c.getMachinePrivKey()
if err != nil {
return nil, err
}
nc, err = newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
if err != nil {
return nil, err
}
@@ -1301,7 +1297,7 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
if err != nil {
return nil, err
}
return np.(*noiseClient), nil
return nc, nil
}
// setDNSNoise sends the SetDNSRequest request to the control plane server over Noise,
@@ -1412,15 +1408,22 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
// 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 {
if pr.URL == "" || !pr.IP.IsValid() || pinger == nil {
logf("invalid ping request: missing url, ip or pinger")
return
}
start := time.Now()
pinger.Ping(pr.IP, pingType, func(res *ipnstate.PingResult) {
// Currently does not check for error since we just return if it fails.
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
})
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(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *tailcfg.PingResponse) error {

View File

@@ -8,10 +8,10 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsdial"
@@ -68,29 +68,25 @@ func TestNewDirect(t *testing.T) {
}
endpoints := fakeEndpoints(1, 2, 3)
changed = c.newEndpoints(12, endpoints)
changed = c.newEndpoints(endpoints)
if !changed {
t.Errorf("c.newEndpoints(12) want true got %v", changed)
t.Errorf("c.newEndpoints want true got %v", changed)
}
changed = c.newEndpoints(12, endpoints)
changed = c.newEndpoints(endpoints)
if changed {
t.Errorf("c.newEndpoints(12) want false got %v", changed)
}
changed = c.newEndpoints(13, endpoints)
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
t.Errorf("c.newEndpoints want false got %v", changed)
}
endpoints = fakeEndpoints(4, 5, 6)
changed = c.newEndpoints(13, endpoints)
changed = c.newEndpoints(endpoints)
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
t.Errorf("c.newEndpoints want true got %v", changed)
}
}
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
for _, port := range ports {
ret = append(ret, tailcfg.Endpoint{
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
Addr: netip.AddrPortFrom(netip.Addr{}, port),
})
}
return

View File

@@ -5,10 +5,11 @@
package controlclient
import (
"fmt"
"log"
"net/netip"
"sort"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
@@ -165,12 +166,6 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
ms.addUserProfile(peer.User)
}
if len(resp.DNS) > 0 {
nm.DNS.Nameservers = resp.DNS
}
if len(resp.SearchPaths) > 0 {
nm.DNS.Domains = resp.SearchPaths
}
if Debug.ProxyDNS {
nm.DNS.Proxied = true
}
@@ -244,7 +239,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
sortNodes(newFull)
}
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 || len(mapRes.PeersChangedPatch) != 0 {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
@@ -265,6 +260,16 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
n.Online = &online
}
}
for _, ec := range mapRes.PeersChangedPatch {
if n, ok := peerByID[ec.NodeID]; ok {
if ec.DERPRegion != 0 {
n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion)
}
if ec.Endpoints != nil {
n.Endpoints = ec.Endpoints
}
}
}
}
mapRes.Peers = newFull
@@ -298,13 +303,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
func filterSelfAddresses(in []netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
switch {
default:
return in
case debugSelfIPv6Only:
for _, a := range in {
if a.IP().Is6() {
if a.Addr().Is6() {
ret = append(ret, a)
}
}

View File

@@ -34,6 +34,16 @@ func TestUndeltaPeers(t *testing.T) {
n.LastSeen = &t
}
}
withDERP := func(d string) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.DERP = d
}
}
withEP := func(ep string) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.Endpoints = []string{ep}
}
}
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
n := &tailcfg.Node{ID: id, Name: name}
for _, f := range mod {
@@ -137,7 +147,53 @@ func TestUndeltaPeers(t *testing.T) {
n(2, "bar", seenAt(time.Unix(123, 0))),
),
},
{
name: "ep_change_derp",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"))),
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
DERPRegion: 4,
}},
},
want: peers(n(1, "foo", withDERP("127.3.3.40:4"))),
},
{
name: "ep_change_udp",
prev: peers(n(1, "foo", withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
Endpoints: []string{"1.2.3.4:56"},
}},
},
want: peers(n(1, "foo", withEP("1.2.3.4:56"))),
},
{
name: "ep_change_udp",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
Endpoints: []string{"1.2.3.4:56"},
}},
},
want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))),
},
{
name: "ep_change_both",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
DERPRegion: 2,
Endpoints: []string{"1.2.3.4:56"},
}},
},
want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.curTime.IsZero() {

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.
//
@@ -40,21 +43,6 @@ import (
"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.

View File

@@ -0,0 +1,61 @@
// 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/types/key"
)
// Variant of Dial that tunnels the request over WebSockets, 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, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
wsScheme := "wss"
wsHost := host
if host == "localhost" {
wsScheme = "ws"
wsHost = addr
}
wsURL := &url.URL{
Scheme: wsScheme,
Host: wsHost,
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 := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
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

@@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/netutil"
"tailscale.com/types/key"
@@ -27,6 +28,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)
@@ -71,3 +75,42 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
return nc, nil
}
// 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)
}
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
return nc, nil
}

View File

@@ -11,13 +11,13 @@ import (
"errors"
"fmt"
"io"
"net/netip"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -156,6 +156,8 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
}
type clientInfo struct {
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// MeshKey optionally specifies a pre-shared key used by
@@ -595,17 +597,17 @@ func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
//
// If the client is broken in some previously detectable way, it
// returns an error.
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
func (c *Client) LocalAddr() (netip.AddrPort, error) {
readErr, _ := c.readErr.Load().(error)
if readErr != nil {
return netaddr.IPPort{}, readErr
return netip.AddrPort{}, readErr
}
if c.nc == nil {
return netaddr.IPPort{}, errors.New("nil conn")
return netip.AddrPort{}, errors.New("nil conn")
}
a := c.nc.LocalAddr()
if a == nil {
return netaddr.IPPort{}, errors.New("nil addr")
return netip.AddrPort{}, errors.New("nil addr")
}
return netaddr.ParseIPPort(a.String())
return netip.ParseAddrPort(a.String())
}

View File

@@ -25,6 +25,7 @@ import (
"math/rand"
"net"
"net/http"
"net/netip"
"os/exec"
"runtime"
"strconv"
@@ -36,7 +37,6 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
@@ -162,8 +162,8 @@ type Server struct {
// src.
sentTo map[key.NodePublic]map[key.NodePublic]int64 // src => dst => dst's latest sclient.connNum
// maps from netaddr.IPPort to a client's public key
keyOfAddr map[netaddr.IPPort]key.NodePublic
// maps from netip.AddrPort to a client's public key
keyOfAddr map[netip.AddrPort]key.NodePublic
}
// clientSet represents 1 or more *sclients.
@@ -314,7 +314,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
watchers: map[*sclient]bool{},
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
avgQueueDuration: new(uint64),
keyOfAddr: map[netaddr.IPPort]key.NodePublic{},
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
}
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
@@ -410,7 +410,7 @@ func (s *Server) IsClientConnectedForTest(k key.NodePublic) bool {
// on its own.
//
// Accept closes nc.
func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
func (s *Server) Accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
closed := make(chan struct{})
s.mu.Lock()
@@ -428,7 +428,7 @@ func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
s.mu.Unlock()
}()
if err := s.accept(nc, brw, remoteAddr, connNum); err != nil && !s.isClosed() {
if err := s.accept(ctx, nc, brw, remoteAddr, connNum); err != nil && !s.isClosed() {
s.logf("derp: %s: %v", remoteAddr, err)
}
}
@@ -641,7 +641,7 @@ func (s *Server) addWatcher(c *sclient) {
go c.requestMeshUpdate()
}
func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
br := brw.Reader
nc.SetDeadline(time.Now().Add(10 * time.Second))
bw := &lazyBufioWriter{w: nc, lbw: brw.Writer}
@@ -660,10 +660,10 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
// At this point we trust the client so we don't time out.
nc.SetDeadline(time.Time{})
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(ctx)
defer cancel()
remoteIPPort, _ := netaddr.ParseIPPort(remoteAddr)
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
c := &sclient{
connNum: connNum,
@@ -1246,7 +1246,7 @@ type sclient struct {
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
@@ -1759,8 +1759,8 @@ type BytesSentRecv struct {
// parseSSOutput parses the output from the specific call to ss in ServeDebugTraffic.
// Separated out for ease of testing.
func parseSSOutput(raw string) map[netaddr.IPPort]BytesSentRecv {
newState := map[netaddr.IPPort]BytesSentRecv{}
func parseSSOutput(raw string) map[netip.AddrPort]BytesSentRecv {
newState := map[netip.AddrPort]BytesSentRecv{}
// parse every 2 lines and get src and dst ips, and kv pairs
lines := strings.Split(raw, "\n")
for i := 0; i < len(lines); i += 2 {
@@ -1768,7 +1768,7 @@ func parseSSOutput(raw string) map[netaddr.IPPort]BytesSentRecv {
if len(ipInfo) < 5 {
continue
}
src, err := netaddr.ParseIPPort(ipInfo[4])
src, err := netip.ParseAddrPort(ipInfo[4])
if err != nil {
continue
}
@@ -1793,7 +1793,7 @@ func parseSSOutput(raw string) map[netaddr.IPPort]BytesSentRecv {
}
func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
prevState := map[netaddr.IPPort]BytesSentRecv{}
prevState := map[netip.AddrPort]BytesSentRecv{}
enc := json.NewEncoder(w)
for r.Context().Err() == nil {
output, err := exec.Command("ss", "-i", "-H", "-t").Output()

View File

@@ -85,8 +85,12 @@ func TestSendRecv(t *testing.T) {
t.Fatal(err)
}
defer cin.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", i))
go s.Accept(ctx, cin, brwServer, fmt.Sprintf("test-client-%d", i))
key := clientPrivateKeys[i]
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
@@ -231,10 +235,10 @@ func TestSendFreeze(t *testing.T) {
// Then cathy stops processing messsages.
// That should not interfere with alice talking to bob.
newClient := func(name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
t.Helper()
c1, c2 := nettest.NewConn(name, 1024)
go s.Accept(c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
go s.Accept(ctx, c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name)
brw := bufio.NewReadWriter(bufio.NewReader(c2), bufio.NewWriter(c2))
c, err := NewClient(k, c2, brw, t.Logf)
@@ -245,14 +249,17 @@ func TestSendFreeze(t *testing.T) {
return c, c2
}
ctx, clientCtxCancel := context.WithCancel(context.Background())
defer clientCtxCancel()
aliceKey := key.NewNode()
aliceClient, aliceConn := newClient("alice", aliceKey)
aliceClient, aliceConn := newClient(ctx, "alice", aliceKey)
bobKey := key.NewNode()
bobClient, bobConn := newClient("bob", bobKey)
bobClient, bobConn := newClient(ctx, "bob", bobKey)
cathyKey := key.NewNode()
cathyClient, cathyConn := newClient("cathy", cathyKey)
cathyClient, cathyConn := newClient(ctx, "cathy", cathyKey)
var (
aliceCh = make(chan struct{}, 32)
@@ -455,7 +462,7 @@ func (ts *testServer) close(t *testing.T) error {
return nil
}
func newTestServer(t *testing.T) *testServer {
func newTestServer(t *testing.T, ctx context.Context) *testServer {
t.Helper()
logf := logger.WithPrefix(t.Logf, "derp-server: ")
s := NewServer(key.NewNode(), logf)
@@ -475,7 +482,7 @@ func newTestServer(t *testing.T) *testServer {
// TODO: register c in ts so Close also closes it?
go func(i int) {
brwServer := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
go s.Accept(c, brwServer, fmt.Sprintf("test-client-%d", i))
go s.Accept(ctx, c, brwServer, fmt.Sprintf("test-client-%d", i))
}(i)
}
}()
@@ -610,7 +617,10 @@ func (c *testClient) close(t *testing.T) {
// TestWatch tests the connection watcher mechanism used by regional
// DERP nodes to mesh up with each other.
func TestWatch(t *testing.T) {
ts := newTestServer(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ts := newTestServer(t, ctx)
defer ts.close(t)
w1 := newTestWatcher(t, ts, "w1")
@@ -1198,7 +1208,10 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
defer connIn.Close()
brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn))
go s.Accept(connIn, brwServer, "test-client")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go s.Accept(ctx, connIn, brwServer, "test-client")
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
client, err := NewClient(k, connOut, brw, logger.Discard)
@@ -1354,7 +1367,10 @@ func TestClientSendRateLimiting(t *testing.T) {
}
func TestServerRepliesToPing(t *testing.T) {
ts := newTestServer(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ts := newTestServer(t, ctx)
defer ts.close(t)
tc := newRegularClient(t, ts, "alice")

View File

@@ -22,6 +22,7 @@ import (
"io/ioutil"
"net"
"net/http"
"net/netip"
"net/url"
"runtime"
"strings"
@@ -30,7 +31,6 @@ import (
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/derp"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
@@ -579,11 +579,11 @@ func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn,
// address (given in s) is valid. An empty value means to dial, but to
// use DNS. The predicate function reports whether the non-empty
// string s contained a valid IP address of the right family.
func shouldDialProto(s string, pred func(netaddr.IP) bool) bool {
func shouldDialProto(s string, pred func(netip.Addr) bool) bool {
if s == "" {
return true
}
ip, _ := netaddr.ParseIP(s)
ip, _ := netip.ParseAddr(s)
return pred(ip)
}
@@ -651,10 +651,10 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
}
}()
}
if shouldDialProto(n.IPv4, netaddr.IP.Is4) {
if shouldDialProto(n.IPv4, netip.Addr.Is4) {
startDial(n.IPv4, "tcp4")
}
if shouldDialProto(n.IPv6, netaddr.IP.Is6) {
if shouldDialProto(n.IPv6, netip.Addr.Is6) {
startDial(n.IPv6, "tcp6")
}
if nwait == 0 {
@@ -839,15 +839,15 @@ func (c *Client) SendPing(data [8]byte) error {
// LocalAddr reports c's local TCP address, without any implicit
// connect or reconnect.
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
func (c *Client) LocalAddr() (netip.AddrPort, error) {
c.mu.Lock()
closed, client := c.closed, c.client
c.mu.Unlock()
if closed {
return netaddr.IPPort{}, ErrClientClosed
return netip.AddrPort{}, ErrClientClosed
}
if client == nil {
return netaddr.IPPort{}, errors.New("client not connected")
return netip.AddrPort{}, errors.New("client not connected")
}
return client.LocalAddr()
}

View File

@@ -56,6 +56,6 @@ func Handler(s *derp.Server) http.Handler {
pubKey.UntypedHexString())
}
s.Accept(netConn, conn, netConn.RemoteAddr().String())
s.Accept(r.Context(), netConn, conn, netConn.RemoteAddr().String())
})
}

View File

@@ -13,7 +13,6 @@ import (
"net"
"nhooyr.io/websocket"
"tailscale.com/derp/wsconn"
)
func init() {
@@ -29,5 +28,6 @@ func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
return nil, err
}
log.Printf("websocket: connected to %v", urlStr)
return wsconn.New(c), nil
netConn := websocket.NetConn(context.Background(), c, websocket.MessageBinary)
return netConn, nil
}

View File

@@ -1,104 +0,0 @@
// 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.
// Package wsconn contains an adapter type that turns
// a websocket connection into a net.Conn.
package wsconn
import (
"context"
"net"
"sync"
"time"
"nhooyr.io/websocket"
)
// New returns a net.Conn wrapper around c,
// using c to send and receive binary messages with
// chunks of bytes with no defined framing, effectively
// discarding all WebSocket-level message framing.
func New(c *websocket.Conn) net.Conn {
return &websocketConn{c: c}
}
// websocketConn implements derp.Conn around a *websocket.Conn,
// treating a websocket.Conn as a byte stream, ignoring the WebSocket
// frame/message boundaries.
type websocketConn struct {
c *websocket.Conn
// rextra are extra bytes owned by the reader.
rextra []byte
mu sync.Mutex
rdeadline time.Time
cancelRead context.CancelFunc
}
func (wc *websocketConn) LocalAddr() net.Addr { return addr{} }
func (wc *websocketConn) RemoteAddr() net.Addr { return addr{} }
type addr struct{}
func (addr) Network() string { return "websocket" }
func (addr) String() string { return "websocket" }
func (wc *websocketConn) Read(p []byte) (n int, err error) {
// Drain any leftover from previously.
n = copy(p, wc.rextra)
if n > 0 {
wc.rextra = wc.rextra[n:]
return n, nil
}
var ctx context.Context
var cancel context.CancelFunc
wc.mu.Lock()
if dl := wc.rdeadline; !dl.IsZero() {
ctx, cancel = context.WithDeadline(context.Background(), wc.rdeadline)
} else {
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(30*24*time.Hour))
wc.rdeadline = time.Time{}
}
wc.cancelRead = cancel
wc.mu.Unlock()
defer cancel()
_, buf, err := wc.c.Read(ctx)
n = copy(p, buf)
wc.rextra = buf[n:]
return n, err
}
func (wc *websocketConn) Write(p []byte) (n int, err error) {
err = wc.c.Write(context.Background(), websocket.MessageBinary, p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (wc *websocketConn) Close() error { return wc.c.Close(websocket.StatusNormalClosure, "close") }
func (wc *websocketConn) SetDeadline(t time.Time) error {
wc.SetReadDeadline(t)
wc.SetWriteDeadline(t)
return nil
}
func (wc *websocketConn) SetReadDeadline(t time.Time) error {
wc.mu.Lock()
defer wc.mu.Unlock()
if !t.IsZero() && (wc.rdeadline.IsZero() || t.Before(wc.rdeadline)) && wc.cancelRead != nil {
wc.cancelRead()
}
wc.rdeadline = t
return nil
}
func (wc *websocketConn) SetWriteDeadline(t time.Time) error {
return nil
}

View File

@@ -24,9 +24,10 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/net/netaddr"
"tailscale.com/types/key"
)
@@ -172,7 +173,7 @@ type CallMeMaybe struct {
// in this field, but might not yet be in control's endpoints.
// (And in the future, control will stop distributing endpoints
// when clients are suitably new.)
MyNumber []netaddr.IPPort
MyNumber []netip.AddrPort
}
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
@@ -180,7 +181,7 @@ const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.IP().As16()
a := ipp.Addr().As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port())
p = p[epLength:]
@@ -193,11 +194,11 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
return m, nil
}
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
m.MyNumber = make([]netip.AddrPort, 0, len(p)/epLength)
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netaddr.IPPortFrom(
m.MyNumber = append(m.MyNumber, netip.AddrPortFrom(
netaddr.IPFrom16(a),
binary.BigEndian.Uint16(p[16:18])))
p = p[epLength:]
@@ -211,7 +212,7 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
// STUN response.
type Pong struct {
TxID [12]byte
Src netaddr.IPPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4
Src netip.AddrPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4
}
const pongLen = 12 + 16 + 2
@@ -219,7 +220,7 @@ const pongLen = 12 + 16 + 2
func (m *Pong) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
d = d[copy(d, m.TxID[:]):]
ip16 := m.Src.IP().As16()
ip16 := m.Src.Addr().As16()
d = d[copy(d, ip16[:]):]
binary.BigEndian.PutUint16(d, m.Src.Port())
return ret
@@ -236,7 +237,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
srcIP, _ := netaddr.FromStdIP(net.IP(p[:16]))
p = p[16:]
port := binary.BigEndian.Uint16(p)
m.Src = netaddr.IPPortFrom(srcIP, port)
m.Src = netip.AddrPortFrom(srcIP, port)
return m, nil
}

View File

@@ -6,12 +6,12 @@ package disco
import (
"fmt"
"net/netip"
"reflect"
"strings"
"testing"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/types/key"
)
@@ -60,9 +60,9 @@ func TestMarshalAndParse(t *testing.T) {
{
name: "call_me_maybe_endpoints",
m: &CallMeMaybe{
MyNumber: []netaddr.IPPort{
netaddr.MustParseIPPort("1.2.3.4:567"),
netaddr.MustParseIPPort("[2001::3456]:789"),
MyNumber: []netip.AddrPort{
netip.MustParseAddrPort("1.2.3.4:567"),
netip.MustParseAddrPort("[2001::3456]:789"),
},
},
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
@@ -93,8 +93,8 @@ func TestMarshalAndParse(t *testing.T) {
}
}
func mustIPPort(s string) netaddr.IPPort {
ipp, err := netaddr.ParseIPPort(s)
func mustIPPort(s string) netip.AddrPort {
ipp, err := netip.ParseAddrPort(s)
if err != nil {
panic(err)
}

View File

@@ -1,7 +0,0 @@
# 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.
FROM ghcr.io/tailscale/tailscale:latest
COPY run.sh /run.sh
CMD "/run.sh"

View File

@@ -1,38 +1,28 @@
# 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.
ifndef IMAGE_TAG
$(error "IMAGE_TAG is not set")
endif
ROUTES ?= ""
TS_ROUTES ?= ""
SA_NAME ?= tailscale
KUBE_SECRET ?= tailscale
build:
@docker build . -t $(IMAGE_TAG)
push: build
@docker push $(IMAGE_TAG)
TS_KUBE_SECRET ?= tailscale
rbac:
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" role.yaml | kubectl apply -f -
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
sidecar:
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
userspace-sidecar:
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | kubectl create -f-
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
proxy:
@kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{DEST_IP}};$(DEST_IP);g" | kubectl create -f-
kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
subnet-router:
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{KUBE_SECRET}};$(KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{IMAGE_TAG}};$(IMAGE_TAG);g" | sed -e "s;{{ROUTES}};$(ROUTES);g" | kubectl create -f-
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-

View File

@@ -1,7 +1,11 @@
# Overview
There are quite a few ways of running Tailscale inside a Kubernetes Cluster, some of the common ones are covered in this doc.
## Instructions
### Setup
1. (Optional) Create the following secret which will automate login.<br>
You will need to get an [auth key](https://tailscale.com/kb/1085/auth-keys/) from [Tailscale Admin Console](https://login.tailscale.com/admin/authkeys).<br>
If you don't provide the key, you can still authenticate using the url in the logs.
@@ -12,26 +16,21 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
metadata:
name: tailscale-auth
stringData:
AUTH_KEY: tskey-...
```
1. Build and push the container
```bash
export IMAGE_TAG=tailscale-k8s:latest
make push
TS_AUTH_KEY: tskey-...
```
1. Tailscale (v1.16+) supports storing state inside a Kubernetes Secret.
Configure RBAC to allow the Tailscale pod to read/write the `tailscale` secret.
```bash
export SA_NAME=tailscale
export KUBE_SECRET=tailscale-auth
export TS_KUBE_SECRET=tailscale-auth
make rbac
```
### Sample Sidecar
Running as a sidecar allows you to directly expose a Kubernetes pod over Tailscale. This is particularly useful if you do not wish to expose a service on the public internet. This method allows bi-directional connectivity between the pod and other devices on the Tailnet. You can use [ACLs](https://tailscale.com/kb/1018/acls/) to control traffic flow.
1. Create and login to the sample nginx pod with a Tailscale sidecar
@@ -47,12 +46,15 @@ Running as a sidecar allows you to directly expose a Kubernetes pod over Tailsca
```bash
curl http://nginx
```
Or, if you have [MagicDNS](https://tailscale.com/kb/1081/magicdns/) disabled:
```bash
curl "http://$(tailscale ip -4 nginx)"
```
#### Userspace Sidecar
You can also run the sidecar in userspace mode. The obvious benefit is reducing the amount of permissions Tailscale needs to run, the downside is that for outbound connectivity from the pod to the Tailnet you would need to use either the [SOCKS proxy](https://tailscale.com/kb/1112/userspace-networking) or HTTP proxy.
1. Create and login to the sample nginx pod with a Tailscale sidecar
@@ -68,25 +70,31 @@ You can also run the sidecar in userspace mode. The obvious benefit is reducing
```bash
curl http://nginx
```
Or, if you have [MagicDNS](https://tailscale.com/kb/1081/magicdns/) disabled:
```bash
curl "http://$(tailscale ip -4 nginx)"
```
### Sample Proxy
Running a Tailscale proxy allows you to provide inbound connectivity to a Kubernetes Service.
1. Provide the `ClusterIP` of the service you want to reach by either:
**Creating a new deployment**
```bash
kubectl create deployment nginx --image nginx
kubectl expose deployment nginx --port 80
export DEST_IP="$(kubectl get svc nginx -o=jsonpath='{.spec.clusterIP}')"
export TS_DEST_IP="$(kubectl get svc nginx -o=jsonpath='{.spec.clusterIP}')"
```
**Using an existing service**
```bash
export DEST_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
export TS_DEST_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
```
1. Deploy the proxy pod
@@ -114,12 +122,12 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
Running a Tailscale [subnet router](https://tailscale.com/kb/1019/subnets/) allows you to access
the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tailscale.
1. Identify the Pod/Service CIDRs that cover your Kubernetes cluster. These will vary depending on [which CNI](https://kubernetes.io/docs/concepts/cluster-administration/networking/) you are using and on the Cloud Provider you are using. Add these to the `ROUTES` variable as comma-separated values.
1. Identify the Pod/Service CIDRs that cover your Kubernetes cluster. These will vary depending on [which CNI](https://kubernetes.io/docs/concepts/cluster-administration/networking/) you are using and on the Cloud Provider you are using. Add these to the `TS_ROUTES` variable as comma-separated values.
```bash
SERVICE_CIDR=10.20.0.0/16
POD_CIDR=10.42.0.0/15
export ROUTES=$SERVICE_CIDR,$POD_CIDR
export TS_ROUTES=$SERVICE_CIDR,$POD_CIDR
```
1. Deploy the subnet-router pod.
@@ -131,7 +139,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
```
1. In the [Tailscale admin console](https://login.tailscale.com/admin/machines), ensure that the
routes for the subnet-router are enabled.
routes for the subnet-router are enabled.
1. Make sure that any client you want to connect from has `--accept-routes` enabled.
@@ -140,8 +148,8 @@ routes for the subnet-router are enabled.
```bash
# Get the Service IP
INTERNAL_IP="$(kubectl get svc <SVC_NAME> -o=jsonpath='{.spec.clusterIP}')"
# or, the Pod IP
# INTERNAL_IP="$(kubectl get po <POD_NAME> -o=jsonpath='{.status.podIP}')"
INTERNAL_PORT=8080
# or, the Pod IP
# INTERNAL_IP="$(kubectl get po <POD_NAME> -o=jsonpath='{.status.podIP}')"
INTERNAL_PORT=8080
curl http://$INTERNAL_IP:$INTERNAL_PORT
```

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