Compare commits

..

52 Commits

Author SHA1 Message Date
James Tucker
20cd690970 .github/workflows: add cross-android
Fixes 4482

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 10:26:40 -07:00
James Tucker
26fe00e7bd .github/workflows: add cross-android
Fixes 4482

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 10:13:27 -07:00
James Tucker
972df80be2 .github/workflows: add cross-android
Fixes 4482

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

Reason: breaks Android build (cgo/pthreads addition)

We can try again next cycle.

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

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

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

Example new output from tailscaled running under my WSL2:

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

Background:

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

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

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

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

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

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

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

Update #4329

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

Updates #3802

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

Updates #3802

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

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

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

Updates #3802

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

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

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

Tested on an LMDE 5 (elsie) VM.

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

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

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

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

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

Previously: 3ae701f0eb

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

Updates #3616

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

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

Updates #3802

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

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

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

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

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

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

Updates #4449
Updates #4293
Updates #3986

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Updates #4217

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

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

Updates #3802

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

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

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

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

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

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

Fixes #4427

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

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

Updates #3802
Updates #4217

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

RELNOTE=start of WhoIsResponse capability support

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

Updates tailscale/corp#754

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

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

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

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

Updates #3802

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

Updates #3802

Change-Id: I756dc2d579a16757537142283d791f1d0319f4f0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-15 13:36:57 -07:00
77 changed files with 2198 additions and 606 deletions

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

@@ -0,0 +1,53 @@
name: Android-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: Android build cmd
env:
GOOS: android
GOARCH: amd64
run: go build ./cmd/...
- name: Android build tests (does not run tests)
env:
GOOS: android
GOARCH: amd64
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -run '^$' -c ); done
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -11,6 +11,9 @@ import "tailscale.com/tailcfg"
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
// Caps are extra capabilities that the remote Node has to this node.
Caps []string `json:",omitempty"`
}
// FileTarget is a node to which files can be sent, and the PeerAPI

View File

@@ -381,6 +381,21 @@ func CheckIPForwarding(ctx context.Context) error {
return nil
}
// CheckPrefs validates the provided preferences, without making any changes.
//
// The CLI uses this before a Start call to fail fast if the preferences won't
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary.
func CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
pj, err := json.Marshal(p)
if err != nil {
return err
}
_, err = send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, bytes.NewReader(pj))
return err
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
if err != nil {

View File

@@ -117,10 +117,32 @@ header.
The `Tailscale-Tailnet` header can help you identify which tailnet the session
is coming from. If you are using node sharing, this can help you make sure that
you aren't giving administrative access to people outside your tailnet. You will
need to be sure to check this in your application code. If you use OpenResty,
you may be able to do more complicated access controls than you can with NGINX
alone.
you aren't giving administrative access to people outside your tailnet.
### Allow Requests From Only One Tailnet
If you want to prevent node sharing from allowing users to access a service, add
the `Expected-Tailnet` header to your auth request:
```nginx
location /auth {
# ...
proxy_set_header Expected-Tailnet "tailscale.com";
}
```
If a user from a different tailnet tries to use that service, this will return a
generic "forbidden" error page:
```html
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
```
## Building

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

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

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

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

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

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

View File

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

View File

@@ -17,6 +17,7 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
@@ -75,6 +76,12 @@ func main() {
return
}
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
w.WriteHeader(http.StatusForbidden)
log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
return
}
h := w.Header()
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
h.Set("Tailscale-User", info.UserProfile.LoginName)

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

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

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

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

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

View File

@@ -86,11 +86,12 @@ func main() {
for i := 0; i < 60; i++ {
st, err := tailscale.Status(context.Background())
if err != nil {
log.Fatal(err)
}
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
log.Printf("error retrieving tailscale status; retrying: %v", err)
} else {
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
}
}
time.Sleep(time.Second)
}

View File

@@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
@@ -21,9 +22,11 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
@@ -106,6 +109,11 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "via",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
},
}
@@ -348,3 +356,46 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
time.Sleep(time.Second)
}
}
func runVia(ctx context.Context, args []string) error {
switch len(args) {
default:
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
case 1:
ipp, err := netaddr.ParseIPPrefix(args[0])
if err != nil {
return err
}
if !ipp.IP().Is6() {
return errors.New("with one argument, expect an IPv6 CIDR")
}
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
return errors.New("not a via route")
}
if ipp.Bits() < 96 {
return errors.New("short length, want /96 or more")
}
v4 := tsaddr.UnmapVia(ipp.IP())
a := ipp.IP().As16()
siteID := binary.BigEndian.Uint32(a[8:12])
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
case 2:
siteID, err := strconv.ParseUint(args[0], 0, 32)
if err != nil {
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
}
if siteID > 0xff {
return fmt.Errorf("site-id values over 255 are currently reserved")
}
ipp, err := netaddr.ParseIPPrefix(args[1])
if err != nil {
return err
}
via, err := tsaddr.MapVia(uint32(siteID), ipp)
if err != nil {
return err
}
fmt.Println(via)
}
return nil
}

View File

@@ -79,9 +79,13 @@ func runSSH(ctx context.Context, args []string) error {
argv := append([]string{
ssh,
// Only trust SSH hosts that we know about.
"-o", fmt.Sprintf("UserKnownHostsFile %s",
shellescape.Quote(knownHostsFile),
),
"-o", "UpdateHostKeys no",
"-o", "StrictHostKeyChecking yes",
"-o", fmt.Sprintf("ProxyCommand %s --socket=%s nc %%h %%p",
shellescape.Quote(tailscaleBin),
shellescape.Quote(rootArgs.socket),

View File

@@ -25,7 +25,6 @@ import (
qrcode "github.com/skip2/go-qrcode"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
@@ -100,9 +99,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
if envknob.UseWIPCode() || inTest() {
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
}
upf.BoolVar(&upArgs.runSSH, "ssh", false, "run an SSH server, permitting access per tailnet admin's declared policy")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.authKeyOrFile, "auth-key", "", `node authorization key; if it begins with "file:", then it's a path to a file containing the authkey`)
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
@@ -595,6 +592,10 @@ func runUp(ctx context.Context, args []string) error {
return err
}
} else {
if err := tailscale.CheckPrefs(ctx, prefs); err != nil {
return err
}
authKey, err := upArgs.getAuthKey()
if err != nil {
return err

View File

@@ -231,7 +231,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh

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

@@ -0,0 +1,12 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || darwin
// +build linux darwin
package main
// Force registration of tailssh with LocalBackend.
import _ "tailscale.com/ssh/tailssh"

View File

@@ -34,6 +34,7 @@ import (
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnstate"
"tailscale.com/log/logheap"
"tailscale.com/logtail"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
@@ -895,6 +896,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
c.logf("exiting process with status %v per controlplane", *code)
os.Exit(*code)
}
if resp.Debug.DisableLogTail {
logtail.Disable()
}
if resp.Debug.LogHeapPprof {
go logheap.LogHeap(resp.Debug.LogHeapURL)
}
@@ -1192,8 +1196,13 @@ func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinge
}
for _, t := range strings.Split(pr.Types, ",") {
switch t {
case "TSMP":
go tsmpPing(logf, c, pr, pinger)
case "TSMP", "disco":
go doPingerPing(logf, c, pr, pinger, t)
// TODO(tailscale/corp#754)
// case "host":
// case "peerapi":
default:
logf("unsupported ping request type: %q", t)
}
}
}
@@ -1389,26 +1398,27 @@ func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
return nc.Do(req)
}
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
// with ping response data.
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) {
// 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 string) {
if pr.URL == "" || pr.IP.IsZero() || pinger == nil {
logf("invalid ping request: missing url, ip or pinger")
return
}
start := time.Now()
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
pinger.Ping(pr.IP, pingType == "TSMP", func(res *ipnstate.PingResult) {
// Currently does not check for error since we just return if it fails.
postPingResult(start, logf, c, pr, res)
postPingResult(start, logf, c, pr, res.ToPingResponse(pingType))
})
}
func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
duration := time.Since(start).Round(time.Millisecond)
func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *tailcfg.PingResponse) error {
duration := time.Since(start)
if pr.Log {
if res.Err == "" {
logf("TSMP ping to %v completed in %v. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration)
logf("ping to %v completed in %v. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration)
} else {
logf("TSMP ping to %v failed after %v: %v", pr.IP, duration, res.Err)
logf("ping to %v failed after %v: %v", pr.IP, duration, res.Err)
}
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
@@ -1424,7 +1434,7 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
}
if pr.Log {
logf("tsmpPing: sending ping results to %v ...", pr.URL)
logf("postPingResult: sending ping results to %v ...", pr.URL)
}
t0 := time.Now()
_, err = c.Do(req)

View File

@@ -113,7 +113,8 @@ func TestTsmpPing(t *testing.T) {
t.Fatal(err)
}
pingRes := &ipnstate.PingResult{
pingRes := &tailcfg.PingResponse{
Type: "TSMP",
IP: "123.456.7890",
Err: "",
NodeName: "testnode",

View File

@@ -538,12 +538,17 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
return tls.Client(nc, tlsConf)
}
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, err error) {
// DialRegionTLS returns a TLS connection to a DERP node in the given region.
//
// DERP nodes for a region are tried in sequence according to their order
// in the DERP map. TLS is initiated on the first node where a socket is
// established.
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
tcpConn, node, err := c.dialRegion(ctx, reg)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
done := make(chan bool) // unbufferd
done := make(chan bool) // unbuffered
defer close(done)
tlsConn = c.tlsClient(tcpConn, node)
@@ -556,13 +561,13 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}()
err = tlsConn.Handshake()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
select {
case done <- true:
return tlsConn, tcpConn, nil
return tlsConn, tcpConn, node, nil
case <-ctx.Done():
return nil, nil, ctx.Err()
return nil, nil, nil, ctx.Err()
}
}

View File

@@ -149,3 +149,9 @@ func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
// if already enabled and any attempt to re-enable it will result in
// an error.
func CanSSHD() bool { return !Bool("TS_DISABLE_SSH_SERVER") }
// SSHPolicyFile returns the path, if any, to the SSHPolicy JSON file for development.
func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }

10
go.mod
View File

@@ -14,6 +14,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
github.com/coreos/go-iptables v0.6.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/creack/pty v1.1.17
github.com/dave/jennifer v1.4.1
github.com/frankban/quicktest v1.14.0
@@ -39,7 +40,7 @@ require (
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
@@ -48,10 +49,10 @@ require (
github.com/u-root/u-root v0.8.0
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
go4.org/mem v0.0.0-20210711025021-927187094b94
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
@@ -105,7 +106,6 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/daixiang0/gci v0.2.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingajkin/go-header v0.4.2 // indirect
@@ -252,7 +252,7 @@ require (
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect

15
go.sum
View File

@@ -1067,8 +1067,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf h1:+DSoknr7gaiW2LlViX6+ko8TBdxTLkvOBbIWQtYyMaE=
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY=
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
@@ -1227,8 +1227,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1478,8 +1478,8 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
@@ -1617,8 +1617,9 @@ golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1/go.mod h1:Uh6Zz+xoGYZom
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=

View File

@@ -1 +1 @@
5ce3ec4d89c72f2a2b6f6f5089c950d7a6a33530
710a0d861098c07540ad073bb73a42ce81bf54a8

View File

@@ -17,11 +17,14 @@ import (
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/version"
)
var started = time.Now()
// New returns a partially populated Hostinfo for the current host.
func New() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
@@ -31,6 +34,7 @@ func New() *tailcfg.Hostinfo {
Hostname: hostname,
OS: version.OS(),
OSVersion: GetOSVersion(),
Desktop: desktop(),
Package: packageTypeCached(),
GoArch: runtime.GOARCH,
DeviceModel: deviceModel(),
@@ -97,6 +101,7 @@ func GetEnvType() EnvType {
var (
deviceModelAtomic atomic.Value // of string
osVersionAtomic atomic.Value // of string
desktopAtomic atomic.Value // of opt.Bool
packagingType atomic.Value // of string
)
@@ -117,6 +122,31 @@ func deviceModel() string {
return s
}
func desktop() (ret opt.Bool) {
if runtime.GOOS != "linux" {
return opt.Bool("")
}
if v := desktopAtomic.Load(); v != nil {
v, _ := v.(opt.Bool)
return v
}
seenDesktop := false
lineread.File("/proc/net/unix", func(line []byte) error {
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
return nil
})
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
if time.Since(started) > time.Minute {
desktopAtomic.Store(ret)
}
return ret
}
func getEnvType() EnvType {
if inKnative() {
return KNative

View File

@@ -259,10 +259,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.8.8:53"},
{Addr: "8.8.8.8"},
},
Routes: map[dnsname.FQDN][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4:53"}},
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
@@ -283,7 +283,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
Hosts: map[dnsname.FQDN][]netaddr.IP{},
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4:53"},
{Addr: "8.8.4.4"},
},
},
},

View File

@@ -73,6 +73,25 @@ func getControlDebugFlags() []string {
return nil
}
// SSHServer is the interface of the conditionally linked ssh/tailssh.server.
type SSHServer interface {
HandleSSHConn(net.Conn) error
// OnPolicyChange is called when the SSH access policy changes,
// so that existing sessions can be re-evaluated for validity
// and closed if they'd no longer be accepted.
OnPolicyChange()
}
type newSSHServerFunc func(logger.Logf, *LocalBackend) (SSHServer, error)
var newSSHServer newSSHServerFunc // or nil
// RegisterNewSSHServer lets the conditionally linked ssh/tailssh package register itself.
func RegisterNewSSHServer(fn newSSHServerFunc) {
newSSHServer = fn
}
// LocalBackend is the glue between the major pieces of the Tailscale
// network software: the cloud control plane (via controlclient), the
// network data plane (via wgengine), and the user-facing UIs and CLIs
@@ -103,14 +122,14 @@ type LocalBackend struct {
newDecompressor func() (controlclient.Decompressor, error)
varRoot string // or empty if SetVarRoot never called
sshAtomicBool syncs.AtomicBool
filterHash deephash.Sum
sshServer SSHServer // or nil
filterAtomic atomic.Value // of *filter.Filter
containsViaIPFuncAtomic atomic.Value // of func(netaddr.IP) bool
// The mutex protects the following elements.
mu sync.Mutex
filterHash deephash.Sum
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
ccGen clientGen // function for producing controlclient; lazily populated
notify func(ipn.Notify)
@@ -205,6 +224,12 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
gotPortPollRes: make(chan struct{}),
loginFlags: loginFlags,
}
if newSSHServer != nil {
b.sshServer, err = newSSHServer(logf, b)
if err != nil {
return nil, fmt.Errorf("newSSHServer: %w", err)
}
}
// Default filter blocks everything and logs nothing, until Start() is called.
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
@@ -295,7 +320,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
// If the local network configuration has changed, our filter may
// need updating to tweak default routes.
b.updateFilter(b.netMap, b.prefs)
b.updateFilterLocked(b.netMap, b.prefs)
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := len(b.netMap.Addresses)
@@ -319,6 +344,7 @@ func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
func (b *LocalBackend) Shutdown() {
b.mu.Lock()
cc := b.cc
b.closePeerAPIListenersLocked()
b.mu.Unlock()
b.unregisterLinkMon()
@@ -505,6 +531,30 @@ func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.Use
return n, u, true
}
// PeerCaps returns the capabilities that remote src IP has to
// ths current node.
func (b *LocalBackend) PeerCaps(src netaddr.IP) []string {
b.mu.Lock()
defer b.mu.Unlock()
if b.netMap == nil {
return nil
}
filt, ok := b.filterAtomic.Load().(*filter.Filter)
if !ok {
return nil
}
for _, a := range b.netMap.Addresses {
if !a.IsSingleIP() {
continue
}
dstIP := a.IP()
if dstIP.BitLen() == src.BitLen() {
return filt.AppendCaps(nil, src, a.IP())
}
}
return nil
}
// SetDecompressor sets a decompression function, which must be a zstd
// reader.
//
@@ -616,7 +666,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
if prefsChanged {
prefs = b.prefs.Clone()
}
if st.NetMap != nil {
b.updateFilterLocked(st.NetMap, prefs)
}
b.mu.Unlock()
// Now complete the lock-free parts of what we started while locked.
@@ -638,7 +690,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
}
b.updateFilter(st.NetMap, prefs)
b.e.SetNetworkMap(st.NetMap)
b.e.SetDERPMap(st.NetMap.DERPMap)
@@ -923,10 +974,9 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.setNetMapLocked(nil)
persistv := b.prefs.Persist
b.updateFilterLocked(nil, nil)
b.mu.Unlock()
b.updateFilter(nil, nil)
if b.portpoll != nil {
b.portpollOnce.Do(func() {
go b.portpoll.Run(b.ctx)
@@ -1024,9 +1074,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return nil
}
// updateFilter updates the packet filter in wgengine based on the
// updateFilterLocked updates the packet filter in wgengine based on the
// given netMap and user preferences.
func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
//
// b.mu must be held.
func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
// NOTE(danderson): keep change detection as the first thing in
// this function. Don't try to optimize by returning early, more
// likely than not you'll just end up breaking the change
@@ -1082,8 +1134,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
}
localNets, _ := localNetsB.IPSet()
logNets, _ := logNetsB.IPSet()
var sshPol tailcfg.SSHPolicy
if haveNetmap && netMap.SSHPolicy != nil {
sshPol = *netMap.SSHPolicy
}
changed := deephash.Update(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
changed := deephash.Update(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp, sshPol)
if !changed {
return
}
@@ -1102,6 +1158,10 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
}
if b.sshServer != nil {
go b.sshServer.OnPolicyChange()
}
}
func (b *LocalBackend) setFilter(f *filter.Filter) {
@@ -1720,11 +1780,53 @@ func (b *LocalBackend) SetCurrentUserID(uid string) {
b.mu.Unlock()
}
func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
b.mu.Lock()
defer b.mu.Unlock()
return b.checkPrefsLocked(p)
}
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
if p.Hostname == "badhostname.tailscale." {
// Keep this one just for testing.
return errors.New("bad hostname [test]")
}
if p.RunSSH {
switch runtime.GOOS {
case "linux":
// okay
case "darwin":
// okay only in tailscaled mode for now.
if version.IsSandboxedMacOS() {
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
}
if !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
}
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !canSSH {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
}
}
return nil
}
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
b.mu.Lock()
p0 := b.prefs.Clone()
p1 := b.prefs.Clone()
p1.ApplyEdits(mp)
if err := b.checkPrefsLocked(p1); err != nil {
b.mu.Unlock()
b.logf("EditPrefs check error: %v", err)
return nil, err
}
if p1.RunSSH && !canSSH {
b.mu.Unlock()
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
@@ -1780,6 +1882,12 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
userID := b.userID
cc := b.cc
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
if caller == "SetPrefs" {
b.logf("SetPrefs: %v", newp.Pretty())
}
b.updateFilterLocked(netMap, newp)
b.mu.Unlock()
if stateKey != "" {
@@ -1789,10 +1897,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
}
b.writeServerModeStartState(userID, newp)
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
if caller == "SetPrefs" {
b.logf("SetPrefs: %v", newp.Pretty())
}
if netMap != nil {
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
if newp.Persist == nil {
@@ -1812,8 +1916,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
b.doSetHostinfoFilterServices(newHi)
}
b.updateFilter(netMap, newp)
if netMap != nil {
b.e.SetDERPMap(netMap.DERPMap)
}
@@ -2110,7 +2212,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
addDefault := func(resolvers []dnstype.Resolver) {
for _, r := range resolvers {
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
}
}
@@ -2139,7 +2241,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
dcfg.Routes[fqdn] = make([]dnstype.Resolver, 0, len(resolvers))
for _, r := range resolvers {
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], r)
}
}
@@ -2170,16 +2272,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
return dcfg
}
func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver {
if ip, err := netaddr.ParseIP(cfg.Addr); err == nil {
// Add 53 here for bare IPs for consistency with previous data type.
return dnstype.Resolver{
Addr: netaddr.IPPortFrom(ip, 53).String(),
}
}
return cfg
}
// SetVarRoot sets the root directory of Tailscale's writable
// storage area . (e.g. "/var/lib/tailscale")
//
@@ -3225,3 +3317,10 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
}
return cc.DoNoiseRequest(req)
}
func (b *LocalBackend) HandleSSHConn(c net.Conn) error {
if b.sshServer == nil {
return errors.New("no SSH server")
}
return b.sshServer.HandleSSHConn(c)
}

View File

@@ -386,6 +386,14 @@ func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64,
}
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
// Android for whatever reason often has problems creating the peerapi listener.
// But since we started intercepting it with netstack, it's not even important that
// we have a real kernel-level listener. So just create a dummy listener on Android
// and let netstack intercept it.
if runtime.GOOS == "android" {
return newFakePeerAPIListener(ip), nil
}
ipStr := ip.String()
var lc net.ListenConfig
@@ -428,8 +436,15 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
return ln, nil
}
}
// Fall back to random ephemeral port.
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
// Fall back to some random ephemeral port.
ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
// And if we're on a platform with netstack (anything but iOS), then just fallback to netstack.
if err != nil && runtime.GOOS != "ios" {
s.b.logf("peerapi: failed to do peerAPI listen, harmless (netstack available) but error was: %v", err)
return newFakePeerAPIListener(ip), nil
}
return ln, err
}
type peerAPIListener struct {
@@ -620,9 +635,29 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
}
}
// canPutFile reports whether h can put a file ("Taildrop") to this node.
func (h *peerAPIHandler) canPutFile() bool {
return h.isSelf || h.peerHasCap(tailcfg.CapabilityFileSharingSend)
}
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
func (h *peerAPIHandler) canDebug() bool {
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
}
func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
for _, hasCap := range h.ps.b.PeerCaps(h.remoteAddr.IP()) {
if hasCap == wantCap {
return true
}
}
return false
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
if !h.canPutFile() {
http.Error(w, "Taildrop access denied", http.StatusForbidden)
return
}
if !h.ps.b.hasCapFileSharing() {
@@ -741,8 +776,8 @@ func approxSize(n int64) string {
}
func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
var buf []byte
@@ -757,8 +792,8 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re
}
func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
var data struct {
@@ -777,8 +812,8 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
}
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
eng := h.ps.b.e
@@ -792,8 +827,8 @@ func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Req
}
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "text/plain")
@@ -1027,3 +1062,49 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
w.Write(j)
return nil
}
// newFakePeerAPIListener creates a new net.Listener that acts like
// it's listening on the provided IP address and on TCP port 1.
//
// See docs on fakePeerAPIListener.
func newFakePeerAPIListener(ip netaddr.IP) net.Listener {
return &fakePeerAPIListener{
addr: netaddr.IPPortFrom(ip, 1).TCPAddr(),
closed: make(chan struct{}),
}
}
// fakePeerAPIListener is a net.Listener that has an Addr method returning a TCPAddr
// for a given IP on port 1 (arbitrary) and can be Closed, but otherwise Accept
// just blocks forever until closed. The purpose of this is to let the rest
// of the LocalBackend/PeerAPI code run and think it's talking to the kernel,
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
// or we lack permission to listen on a port. It's okay to not actually listen via
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
// also intercept netstack TCP requests in to our peerapi port and hand it over
// directly to peerapi, without involving the kernel. So this doesn't need to be
// real. But the port number we return (1, in this case) is the port number we advertise
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's
// using it, it doesn't matter, as we intercept it first in netstack and the kernel
// never notices.
//
// Eventually we'll remove this code and do this on all platforms, when iOS also uses
// netstack.
type fakePeerAPIListener struct {
addr net.Addr
closeOnce sync.Once
closed chan struct{}
}
func (fl *fakePeerAPIListener) Close() error {
fl.closeOnce.Do(func() { close(fl.closed) })
return nil
}
func (fl *fakePeerAPIListener) Accept() (net.Conn, error) {
<-fl.closed
return nil, io.EOF
}
func (fl *fakePeerAPIListener) Addr() net.Addr { return fl.addr }

View File

@@ -158,7 +158,7 @@ func TestHandlePeerAPI(t *testing.T) {
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("not owner"),
bodyContains("Taildrop access denied"),
),
},
{

View File

@@ -480,6 +480,8 @@ func osEmoji(os string) string {
// PingResult contains response information for the "tailscale ping" subcommand,
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
// See tailcfg.PingResponse for a related response that is sent back to control
// for remote diagnostic pings.
type PingResult struct {
IP string // ping destination
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
@@ -513,6 +515,22 @@ type PingResult struct {
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
func (pr *PingResult) ToPingResponse(pingType string) *tailcfg.PingResponse {
return &tailcfg.PingResponse{
Type: pingType,
IP: pr.IP,
NodeIP: pr.NodeIP,
NodeName: pr.NodeName,
Err: pr.Err,
LatencySeconds: pr.LatencySeconds,
Endpoint: pr.Endpoint,
DERPRegionID: pr.DERPRegionID,
DERPRegionCode: pr.DERPRegionCode,
PeerAPIPort: pr.PeerAPIPort,
IsLocalIP: pr.IsLocalIP,
}
}
func SortPeers(peers []*PeerStatus) {
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
}

View File

@@ -111,6 +111,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveLogout(w, r)
case "/localapi/v0/prefs":
h.servePrefs(w, r)
case "/localapi/v0/check-prefs":
h.serveCheckPrefs(w, r)
case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r)
case "/localapi/v0/bugreport":
@@ -223,6 +225,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
res := &apitype.WhoIsResponse{
Node: n,
UserProfile: &u,
Caps: b.PeerCaps(ipp.IP()),
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
@@ -375,7 +378,9 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
var err error
prefs, err = h.b.EditPrefs(mp)
if err != nil {
http.Error(w, err.Error(), 400)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(resJSON{Error: err.Error()})
return
}
case "GET", "HEAD":
@@ -390,6 +395,33 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
e.Encode(prefs)
}
type resJSON struct {
Error string `json:",omitempty"`
}
func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "checkprefs access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
p := new(ipn.Prefs)
if err := json.NewDecoder(r.Body).Decode(p); err != nil {
http.Error(w, "invalid JSON body", 400)
return
}
err := h.b.CheckPrefs(p)
var res resJSON
if err != nil {
res.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "file access denied", http.StatusForbidden)

View File

@@ -20,6 +20,7 @@ import (
"tailscale.com/logtail/backoff"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
tslogger "tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
@@ -412,7 +413,18 @@ func (l *Logger) Flush() error {
return nil
}
// logtailDisabled is whether logtail uploads to logcatcher are disabled.
var logtailDisabled syncs.AtomicBool
// Disable disables logtail uploads for the lifetime of the process.
func Disable() {
logtailDisabled.Set(true)
}
func (l *Logger) send(jsonBlob []byte) (int, error) {
if logtailDisabled.Get() {
return len(jsonBlob), nil
}
n, err := l.buffer.Write(jsonBlob)
if l.drainLogs == nil {
select {

View File

@@ -84,10 +84,7 @@ func (c Config) hasDefaultIPResolversOnly() bool {
return false
}
for _, r := range c.DefaultResolvers {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
continue
}
if _, err := netaddr.ParseIP(r.Addr); err != nil {
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 {
return false
}
}

View File

@@ -174,7 +174,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
}
var defaultRoutes []dnstype.Resolver
for _, ip := range bcfg.Nameservers {
defaultRoutes = append(defaultRoutes, dnstype.ResolverFromIP(ip))
defaultRoutes = append(defaultRoutes, dnstype.Resolver{Addr: ip.String()})
}
rcfg.Routes["."] = defaultRoutes
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
@@ -188,23 +188,13 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
func toIPsOnly(resolvers []dnstype.Resolver) (ret []netaddr.IP) {
for _, r := range resolvers {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
ret = append(ret, ipp.IP())
} else if ip, err := netaddr.ParseIP(r.Addr); err == nil {
ret = append(ret, ip)
}
}
return ret
}
func toIPPorts(ips []netaddr.IP) (ret []netaddr.IPPort) {
ret = make([]netaddr.IPPort, 0, len(ips))
for _, ip := range ips {
ret = append(ret, netaddr.IPPortFrom(ip, 53))
}
return ret
}
func (m *Manager) EnqueuePacket(bs []byte, proto ipproto.Proto, from, to netaddr.IPPort) error {
return m.resolver.EnqueuePacket(bs, proto, from, to)
}

View File

@@ -96,7 +96,7 @@ func TestManager(t *testing.T) {
{
name: "corp",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
@@ -107,7 +107,7 @@ func TestManager(t *testing.T) {
{
name: "corp-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -119,7 +119,7 @@ func TestManager(t *testing.T) {
{
name: "corp-magic",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Routes: upstreams("ts.com", ""),
Hosts: hosts(
@@ -131,7 +131,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -141,7 +141,7 @@ func TestManager(t *testing.T) {
{
name: "corp-magic-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Routes: upstreams("ts.com", ""),
Hosts: hosts(
@@ -154,7 +154,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -164,8 +164,8 @@ func TestManager(t *testing.T) {
{
name: "corp-routes",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
@@ -174,15 +174,15 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
".", "1.1.1.1", "9.9.9.9",
"corp.com.", "2.2.2.2"),
},
},
{
name: "corp-routes-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -192,14 +192,14 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
".", "1.1.1.1", "9.9.9.9",
"corp.com.", "2.2.2.2"),
},
},
{
name: "routes",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
@@ -212,14 +212,14 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
".", "8.8.8.8",
"corp.com.", "2.2.2.2"),
},
},
{
name: "routes-split",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -233,8 +233,8 @@ func TestManager(t *testing.T) {
name: "routes-multi",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
@@ -247,17 +247,17 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
".", "8.8.8.8",
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
},
{
name: "routes-multi-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -268,8 +268,8 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
},
{
@@ -290,7 +290,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(".", "8.8.8.8:53"),
Routes: upstreams(".", "8.8.8.8"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -322,7 +322,7 @@ func TestManager(t *testing.T) {
{
name: "routes-magic",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53", "ts.com", ""),
Routes: upstreams("corp.com", "2.2.2.2", "ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -338,8 +338,8 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
".", "8.8.8.8:53"),
"corp.com.", "2.2.2.2",
".", "8.8.8.8"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -350,7 +350,7 @@ func TestManager(t *testing.T) {
name: "routes-magic-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"corp.com", "2.2.2.2",
"ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
@@ -364,7 +364,7 @@ func TestManager(t *testing.T) {
MatchDomains: fqdns("corp.com", "ts.com"),
},
rs: resolver.Config{
Routes: upstreams("corp.com.", "2.2.2.2:53"),
Routes: upstreams("corp.com.", "2.2.2.2"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -393,6 +393,14 @@ func TestManager(t *testing.T) {
},
}
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string {
if ipp.Port() == 53 {
return ipp.IP().String()
}
return ipp.String()
})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := fakeOSConfigurator{
@@ -405,8 +413,6 @@ func TestManager(t *testing.T) {
if err := m.Set(test.in); err != nil {
t.Fatalf("m.Set: %v", err)
}
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string { return ipp.String() })
if diff := cmp.Diff(f.OSConfig, test.os, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
}
@@ -503,6 +509,11 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
panic("IPPort provided before suffix")
}
ret[key] = append(ret[key], dnstype.Resolver{Addr: ipp.String()})
} else if _, err := netaddr.ParseIP(s); err == nil {
if key == "" {
panic("IPPort provided before suffix")
}
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
} else if strings.HasPrefix(s, "http") {
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
} else {

View File

@@ -49,9 +49,9 @@ func TestDoH(t *testing.T) {
dohSem: make(chan struct{}, 10),
}
for ip := range publicdns.KnownDoH() {
t.Run(ip.String(), func(t *testing.T) {
urlBase, c, ok := f.getKnownDoHClient(ip)
for urlBase := range publicdns.DoHIPsOfBase() {
t.Run(urlBase, func(t *testing.T) {
c, ok := f.getKnownDoHClientForProvider(urlBase)
if !ok {
t.Fatal("expected DoH")
}

View File

@@ -15,6 +15,7 @@ import (
"math/rand"
"net"
"net/http"
"net/url"
"runtime"
"sort"
"strconv"
@@ -24,8 +25,10 @@ import (
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dnscache"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
@@ -47,6 +50,11 @@ const (
// arbitrary.
dohTransportTimeout = 30 * time.Second
// dohTransportTimeout is how much of a head start to give a DoH query
// that was upgraded from a well-known public DNS provider's IP before
// normal UDP mode is attempted as a fallback.
dohHeadStart = 500 * time.Millisecond
// wellKnownHostBackupDelay is how long to artificially delay upstream
// DNS queries to the "fallback" DNS server IP for a known provider
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
@@ -226,65 +234,55 @@ func (f *forwarder) Close() error {
return nil
}
// resolversWithDelays maps from a set of DNS server names to a slice of
// a type that included a startDelay. So if resolvers contains e.g. four
// Google DNS IPs (two IPv4 + twoIPv6), this function partition adds
// delays to some.
// resolversWithDelays maps from a set of DNS server names to a slice of a type
// that included a startDelay, upgrading any well-known DoH (DNS-over-HTTP)
// servers in the process, insert a DoH lookup first before UDP fallbacks.
func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
rr := make([]resolverAndDelay, 0, len(resolvers)+2)
// Add the known DoH ones first, starting immediately.
didDoH := map[string]bool{}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
continue
}
dohBase, ok := publicdns.KnownDoH()[ipp.IP()]
if !ok || didDoH[dohBase] {
continue
}
didDoH[dohBase] = true
rr = append(rr, resolverAndDelay{name: dnstype.Resolver{Addr: dohBase}})
}
type hostAndFam struct {
host string // some arbitrary string representing DNS host (currently the DoH base)
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
}
// Track how many of each known resolver host are in the list,
// per address family.
total := map[hostAndFam]int{}
rr := make([]resolverAndDelay, len(resolvers))
for _, r := range resolvers {
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
if host, ok := publicdns.KnownDoH()[ip]; ok {
total[hostAndFam{host, ip.BitLen()}]++
}
}
}
done := map[hostAndFam]int{}
for i, r := range resolvers {
var startDelay time.Duration
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
if host, ok := publicdns.KnownDoH()[ip]; ok {
key4 := hostAndFam{host, 32}
key6 := hostAndFam{host, 128}
switch {
case ip.Is4():
if done[key4] > 0 {
startDelay += wellKnownHostBackupDelay
}
case ip.Is6():
total4 := total[key4]
if total4 >= 2 {
// If we have two IPv4 IPs of the same provider
// already in the set, delay the IPv6 queries
// until halfway through the timeout (so wait
// 2.5 seconds). Even the network is IPv6-only,
// the DoH dialer will fallback to IPv6
// immediately anyway.
startDelay = responseTimeout / 2
} else if total4 == 1 {
startDelay += wellKnownHostBackupDelay
}
if done[key6] > 0 {
startDelay += wellKnownHostBackupDelay
}
}
done[hostAndFam{host, ip.BitLen()}]++
}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
// Pass non-IP ones through unchanged, without delay.
// (e.g. DNS-over-ExitDNS when using an exit node)
rr = append(rr, resolverAndDelay{name: r})
continue
}
rr[i] = resolverAndDelay{
ip := ipp.IP()
var startDelay time.Duration
if host, ok := publicdns.KnownDoH()[ip]; ok {
// We already did the DoH query early. These
startDelay = dohHeadStart
key := hostAndFam{host, ip.BitLen()}
if done[key] > 0 {
startDelay += wellKnownHostBackupDelay
}
done[key]++
}
rr = append(rr, resolverAndDelay{
name: r,
startDelay: startDelay,
}
})
}
return rr
}
@@ -332,21 +330,30 @@ func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
return lc, nil
}
func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
urlBase, ok = publicdns.KnownDoH()[ip]
if !ok {
return
}
// getKnownDoHClientForProvider returns an HTTP client for a specific DoH
// provider named by its DoH base URL (like "https://dns.google/dns-query").
//
// The returned client race/Happy Eyeballs dials all IPs for urlBase (usually
// 4), as statically known by the publicdns package.
func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client, ok bool) {
f.mu.Lock()
defer f.mu.Unlock()
if c, ok := f.dohClient[urlBase]; ok {
return urlBase, c, true
return c, true
}
if f.dohClient == nil {
f.dohClient = map[string]*http.Client{}
allIPs := publicdns.DoHIPsOfBase()[urlBase]
if len(allIPs) == 0 {
return nil, false
}
dohURL, err := url.Parse(urlBase)
if err != nil {
return nil, false
}
nsDialer := netns.NewDialer(f.logf)
dialer := dnscache.Dialer(nsDialer.DialContext, &dnscache.Resolver{
SingleHost: dohURL.Hostname(),
SingleHostStaticResult: allIPs,
})
c = &http.Client{
Transport: &http.Transport{
IdleConnTimeout: dohTransportTimeout,
@@ -354,21 +361,15 @@ func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Cl
if !strings.HasPrefix(netw, "tcp") {
return nil, fmt.Errorf("unexpected network %q", netw)
}
c, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), "443"))
// If v4 failed, try an equivalent v6 also in the time remaining.
if err != nil && ctx.Err() == nil {
if ip6, ok := publicdns.DoHV6(urlBase); ok && ip.Is4() {
if c6, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip6.String(), "443")); err == nil {
return c6, nil
}
}
}
return c, err
return dialer(ctx, netw, addr)
},
},
}
if f.dohClient == nil {
f.dohClient = map[string]*http.Client{}
}
f.dohClient[urlBase] = c
return urlBase, c, true
return c, true
}
const dohType = "application/dns-message"
@@ -422,34 +423,43 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
return res, err
}
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if verboseDNSForward {
f.logf("forwarder.send(%q) ...", rr.name.Addr)
defer func() {
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)
}()
}
if strings.HasPrefix(rr.name.Addr, "http://") {
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
}
if strings.HasPrefix(rr.name.Addr, "https://") {
// Only known DoH providers are supported currently. Specifically, we
// only support DoH providers where we can TCP connect to them on port
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
// aren't currently supported. There's no backup DNS resolution path for
// them.
urlBase := rr.name.Addr
if hc, ok := f.getKnownDoHClientForProvider(urlBase); ok {
return f.sendDoH(ctx, urlBase, hc, fq.packet)
}
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("https:// resolvers not supported yet")
return nil, fmt.Errorf("arbitrary https:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "tls://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("tls:// resolvers not supported yet")
}
ipp, err := netaddr.ParseIPPort(rr.name.Addr)
if err != nil {
return nil, err
}
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
// All known DoH is over port 53.
if urlBase, dc, ok := f.getKnownDoHClient(ipp.IP()); ok {
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
if err == nil || ctx.Err() != nil {
return res, err
}
f.logf("DoH error from %v: %v", ipp.IP(), err)
ipp, ok := rr.name.IPPort()
if !ok {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
}
metricDNSFwdUDP.Add(1)
@@ -593,6 +603,10 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
return err
}
// Guarantee that the ctx we use below is done when this function returns.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Drop DNS service discovery spam, primarily for battery life
// on mobile. Things like Spotify on iOS generate this traffic,
// when browsing for LAN devices. But even when filtering this
@@ -633,12 +647,8 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
defer fq.closeOnCtxDone.Close()
resc := make(chan []byte, 1)
var (
mu sync.Mutex
firstErr error
)
resc := make(chan []byte, 1) // it's fine buffered or not
errc := make(chan error, 1) // it's fine buffered or not too
for i := range resolvers {
go func(rr *resolverAndDelay) {
if rr.startDelay > 0 {
@@ -652,39 +662,48 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
resb, err := f.send(ctx, fq, *rr)
if err != nil {
mu.Lock()
defer mu.Unlock()
if firstErr == nil {
firstErr = err
select {
case errc <- err:
case <-ctx.Done():
}
return
}
select {
case resc <- resb:
default:
case <-ctx.Done():
}
}(&resolvers[i])
}
select {
case v := <-resc:
var firstErr error
var numErr int
for {
select {
case v := <-resc:
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return ctx.Err()
case responseChan <- packet{v, query.addr}:
metricDNSFwdSuccess.Add(1)
return nil
}
case err := <-errc:
if firstErr == nil {
firstErr = err
}
numErr++
if numErr == len(resolvers) {
return firstErr
}
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
return ctx.Err()
case responseChan <- packet{v, query.addr}:
metricDNSFwdSuccess.Add(1)
return nil
}
case <-ctx.Done():
mu.Lock()
defer mu.Unlock()
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
return ctx.Err()
}
}

View File

@@ -5,8 +5,8 @@
package resolver
import (
"flag"
"fmt"
"net"
"reflect"
"strings"
"testing"
@@ -24,11 +24,7 @@ func (rr resolverAndDelay) String() string {
func TestResolversWithDelays(t *testing.T) {
// query
q := func(ss ...string) (ipps []dnstype.Resolver) {
for _, s := range ss {
host, _, err := net.SplitHostPort(s)
if err != nil {
t.Fatal(err)
}
for _, host := range ss {
ipps = append(ipps, dnstype.Resolver{Addr: host})
}
return
@@ -45,12 +41,8 @@ func TestResolversWithDelays(t *testing.T) {
panic(fmt.Sprintf("parsing duration in %q: %v", s, err))
}
}
host, _, err := net.SplitHostPort(s)
if err != nil {
t.Fatal(err)
}
rr = append(rr, resolverAndDelay{
name: dnstype.Resolver{Addr: host},
name: dnstype.Resolver{Addr: s},
startDelay: d,
})
}
@@ -64,28 +56,28 @@ func TestResolversWithDelays(t *testing.T) {
}{
{
name: "unknown-no-delays",
in: q("1.2.3.4:53", "2.3.4.5:53"),
want: o("1.2.3.4:53", "2.3.4.5:53"),
in: q("1.2.3.4", "2.3.4.5"),
want: o("1.2.3.4", "2.3.4.5"),
},
{
name: "google-all-ipv4",
in: q("8.8.8.8:53", "8.8.4.4:53"),
want: o("8.8.8.8:53", "8.8.4.4:53+200ms"),
in: q("8.8.8.8", "8.8.4.4"),
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s"),
},
{
name: "google-only-ipv6",
in: q("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
want: o("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53+200ms"),
in: q("2001:4860:4860::8888", "2001:4860:4860::8844"),
want: o("https://dns.google/dns-query", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
},
{
name: "google-all-four",
in: q("8.8.8.8:53", "8.8.4.4:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
want: o("8.8.8.8:53", "8.8.4.4:53+200ms", "[2001:4860:4860::8888]:53+2.5s", "[2001:4860:4860::8844]:53+2.7s"),
in: q("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"),
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
},
{
name: "quad9-one-v4-one-v6",
in: q("9.9.9.9:53", "[2620:fe::fe]:53"),
want: o("9.9.9.9:53", "[2620:fe::fe]:53+200ms"),
in: q("9.9.9.9", "2620:fe::fe"),
want: o("https://dns.quad9.net/dns-query", "9.9.9.9+0.5s", "2620:fe::fe+0.5s"),
},
}
@@ -169,6 +161,25 @@ func TestMaxDoHInFlight(t *testing.T) {
}
}
var testDNS = flag.Bool("test-dns", false, "run tests that require a working DNS server")
func TestGetKnownDoHClientForProvider(t *testing.T) {
var fwd forwarder
c, ok := fwd.getKnownDoHClientForProvider("https://dns.google/dns-query")
if !ok {
t.Fatal("not found")
}
if !*testDNS {
t.Skip("skipping without --test-dns")
}
res, err := c.Head("https://dns.google/")
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
t.Logf("Got: %+v", res)
}
func BenchmarkNameFromQuery(b *testing.B) {
builder := dns.NewBuilder(nil, dns.Header{})
builder.StartQuestions()

View File

@@ -725,6 +725,22 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
return "", dns.RCodeRefused
}
r.mu.Lock()
defer r.mu.Unlock()
// If the requested IP is part of the IPv6 4-to-6 range, it might
// correspond to an IPv4 address (assuming IPv4 is enabled).
if ip4, ok := tsaddr.Tailscale6to4(ip); ok {
fqdn, code := r.fqdnForIPLocked(ip4, name)
if code == dns.RCodeSuccess {
return fqdn, code
}
}
return r.fqdnForIPLocked(ip, name)
}
// r.mu must be held.
func (r *Resolver) fqdnForIPLocked(ip netaddr.IP, name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
// If someone curiously does a reverse lookup on the DNS IP, we
// return a domain that helps indicate that Tailscale is using
// this IP for a special purpose and it is not a node on their
@@ -733,8 +749,6 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
return dnsSymbolicFQDN, dns.RCodeSuccess
}
r.mu.Lock()
defer r.mu.Unlock()
ret, ok := r.ipToHost[ip]
if !ok {
for _, suffix := range r.localDomains {

View File

@@ -382,6 +382,7 @@ func TestResolveLocalReverse(t *testing.T) {
{"ipv6_nxdomain", dnsname.FQDN("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.ip6.arpa."), "", dns.RCodeNameError},
{"nxdomain", dnsname.FQDN("2.3.4.5.in-addr.arpa."), "", dns.RCodeRefused},
{"magicdns", dnsname.FQDN("100.100.100.100.in-addr.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
{"ipv6_4to6", dnsname.FQDN("4.6.4.6.4.6.2.6.6.9.d.c.3.4.8.4.2.1.b.a.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
}
for _, tt := range tests {
@@ -755,6 +756,9 @@ func TestDelegateCollision(t *testing.T) {
if err != nil {
t.Error(err)
}
if len(ans) == 0 {
t.Fatal("no answers")
}
var wantType dns.Type
switch ans[0].Body.(type) {

View File

@@ -72,6 +72,15 @@ type Resolver struct {
// if a refresh fails.
UseLastGood bool
// SingleHostStaticResult, if non-nil, is the static result of IPs that is returned
// by Resolver.LookupIP for any hostname. When non-nil, SingleHost must also be
// set with the expected name.
SingleHostStaticResult []netaddr.IP
// SingleHost is the hostname that SingleHostStaticResult is for.
// It is required when SingleHostStaticResult is present.
SingleHost string
sf singleflight.Group
mu sync.Mutex
@@ -108,6 +117,22 @@ var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
// If err is nil, ip will be non-nil. The v6 address may be nil even
// with a nil error.
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, allIPs []net.IPAddr, err error) {
if r.SingleHostStaticResult != nil {
if r.SingleHost != host {
return nil, nil, nil, fmt.Errorf("dnscache: unexpected hostname %q doesn't match expected %q", host, r.SingleHost)
}
for _, naIP := range r.SingleHostStaticResult {
ipa := naIP.IPAddr()
if ip == nil && naIP.Is4() {
ip = ipa.IP
}
if v6 == nil && naIP.Is6() {
v6 = ipa.IP
}
allIPs = append(allIPs, *ipa)
}
return
}
if ip := net.ParseIP(host); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
return ip4, nil, []net.IPAddr{{IP: ip4}}, nil

View File

@@ -8,6 +8,7 @@ import (
"context"
"errors"
"flag"
"fmt"
"net"
"reflect"
"testing"
@@ -110,3 +111,33 @@ func TestDialCall_uniqueIPs(t *testing.T) {
t.Errorf("got %v; want %v", got, want)
}
}
func TestResolverAllHostStaticResult(t *testing.T) {
r := &Resolver{
SingleHost: "foo.bar",
SingleHostStaticResult: []netaddr.IP{
netaddr.MustParseIP("2001:4860:4860::8888"),
netaddr.MustParseIP("2001:4860:4860::8844"),
netaddr.MustParseIP("8.8.8.8"),
netaddr.MustParseIP("8.8.4.4"),
},
}
ip4, ip6, allIPs, err := r.LookupIP(context.Background(), "foo.bar")
if err != nil {
t.Fatal(err)
}
if got, want := ip4.String(), "8.8.8.8"; got != want {
t.Errorf("ip4 got %q; want %q", got, want)
}
if got, want := ip6.String(), "2001:4860:4860::8888"; got != want {
t.Errorf("ip4 got %q; want %q", got, want)
}
if got, want := fmt.Sprintf("%q", allIPs), `[{"2001:4860:4860::8888" ""} {"2001:4860:4860::8844" ""} {"8.8.8.8" ""} {"8.8.4.4" ""}]`; got != want {
t.Errorf("allIPs got %q; want %q", got, want)
}
_, _, _, err = r.LookupIP(context.Background(), "bad")
if got, want := fmt.Sprint(err), `dnscache: unexpected hostname "bad" doesn't match expected "foo.bar"`; got != want {
t.Errorf("bad dial error got %q; want %q", got, want)
}
}

View File

@@ -6,18 +6,25 @@
"RegionName": "r1",
"Nodes": [
{
"Name": "1a",
"Name": "1c",
"RegionID": 1,
"HostName": "derp1.tailscale.com",
"IPv4": "159.89.225.99",
"IPv6": "2604:a880:400:d1::828:b001"
"HostName": "derp1c.tailscale.com",
"IPv4": "104.248.8.210",
"IPv6": "2604:a880:800:10::7a0:e001"
},
{
"Name": "1b",
"Name": "1d",
"RegionID": 1,
"HostName": "derp1b.tailscale.com",
"IPv4": "45.55.35.93",
"IPv6": "2604:a880:800:a1::f:2001"
"HostName": "derp1d.tailscale.com",
"IPv4": "165.22.33.71",
"IPv6": "2604:a880:800:10::7fe:f001"
},
{
"Name": "1e",
"RegionID": 1,
"HostName": "derp1e.tailscale.com",
"IPv4": "64.225.56.166",
"IPv6": "2604:a880:800:10::873:4001"
}
]
},

View File

@@ -979,13 +979,21 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
// One warm-up one to get HTTP connection set
// up and get a connection from the browser's
// pool.
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
if err != nil {
c.logf("probing %s: %v", node.HostName, err)
} else {
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
}
return
}
t0 := c.timeNow()
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
if err != nil {
c.logf("probing %s: %v", node.HostName, err)
} else {
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
}
return
}
d := c.timeNow().Sub(t0)
@@ -1005,7 +1013,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
var ip netaddr.IP
dc := derphttp.NewNetcheckClient(c.logf)
tlsConn, tcpConn, err := dc.DialRegionTLS(ctx, reg)
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
if err != nil {
return 0, ip, err
}
@@ -1036,7 +1044,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
hc := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://derp-unused-hostname.tld/derp/latency-check", nil)
req, err := http.NewRequestWithContext(ctx, "GET", "https://" + node.HostName + "/derp/latency-check", nil)
if err != nil {
return 0, ip, err
}
@@ -1047,6 +1055,13 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
defer resp.Body.Close()
// DERPs should give us a nominal status code, so anything else is probably
// an access denied by a MITM proxy (or at the very least a signal not to
// trust this latency check).
if resp.StatusCode > 299 {
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
}
_, err = io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8<<10))
if err != nil {
return 0, ip, err

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 linux && !android
// +build linux,!android
package netns
import (

View File

@@ -6,6 +6,8 @@
package tsaddr
import (
"encoding/binary"
"errors"
"sync"
"inet.af/netaddr"
@@ -126,6 +128,18 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
return netaddr.IPFrom16(ret)
}
// Tailscale6to4 returns the IPv4 address corresponding to the given
// tailscale IPv6 address within the 4To6 range. The IPv4 address
// and true are returned if the given address was in the correct range,
// false if not.
func Tailscale6to4(ipv6 netaddr.IP) (netaddr.IP, bool) {
if !ipv6.Is6() || !Tailscale4To6Range().Contains(ipv6) {
return netaddr.IP{}, false
}
v6 := ipv6.As16()
return netaddr.IPv4(100, v6[13], v6[14], v6[15]), true
}
func mustPrefix(v *netaddr.IPPrefix, prefix string) {
var err error
*v, err = netaddr.ParseIPPrefix(prefix)
@@ -280,3 +294,17 @@ func UnmapVia(ip netaddr.IP) netaddr.IP {
}
return ip
}
// MapVia returns an IPv6 "via" route for an IPv4 CIDR in a given siteID.
func MapVia(siteID uint32, v4 netaddr.IPPrefix) (via netaddr.IPPrefix, err error) {
if !v4.IP().Is4() {
return via, errors.New("want IPv4 CIDR with a site ID")
}
viaRange16 := TailscaleViaRange().IP().As16()
var a [16]byte
copy(a[:], viaRange16[:8])
binary.BigEndian.PutUint32(a[8:], siteID)
ip4a := v4.IP().As4()
copy(a[12:], ip4a[:])
return netaddr.IPPrefixFrom(netaddr.IPFrom16(a), v4.Bits()+64+32), nil
}

View File

@@ -41,16 +41,11 @@ main() {
# - ID: the short name of the OS (e.g. "debian", "freebsd")
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
# - UBUNTU_CODENAME: if it exists, as in linuxmint, use instead of VERSION_CODENAME
. /etc/os-release
case "$ID" in
ubuntu|pop|neon|zorin|elementary|linuxmint)
ubuntu|pop|neon|zorin|elementary)
OS="ubuntu"
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
VERSION="$UBUNTU_CODENAME"
else
VERSION="$VERSION_CODENAME"
fi
VERSION="$VERSION_CODENAME"
PACKAGETYPE="apt"
# Third-party keyrings became the preferred method of
# installation in Ubuntu 20.04.
@@ -72,6 +67,35 @@ main() {
APT_KEY_TYPE="keyring"
fi
;;
linuxmint)
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
OS="ubuntu"
VERSION="$UBUNTU_CODENAME"
elif [ "${DEBIAN_CODENAME:-}" != "" ]; then
OS="debian"
VERSION="$DEBIAN_CODENAME"
else
OS="ubuntu"
VERSION="$VERSION_CODENAME"
fi
PACKAGETYPE="apt"
if [ "$VERSION_ID" -lt 5 ]; then
APT_KEY_TYPE="legacy"
else
APT_KEY_TYPE="keyring"
fi
;;
parrot)
OS="debian"
PACKAGETYPE="apt"
if [ "$VERSION_ID" -lt 5 ]; then
VERSION="buster"
APT_KEY_TYPE="legacy"
else
VERSION="bullseye"
APT_KEY_TYPE="keyring"
fi
;;
raspbian)
OS="$ID"
VERSION="$VERSION_CODENAME"
@@ -127,7 +151,7 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky)
rocky|almalinux)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
@@ -474,7 +498,7 @@ main() {
;;
emerge)
set -x
$SUDO emerge net-vpn/tailscale
$SUDO emerge --ask=n net-vpn/tailscale
set +x
;;
appstore)

View File

@@ -59,11 +59,11 @@ var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remot
// If ss.srv.tailscaledPath is empty, this method is equivalent to
// exec.CommandContext.
func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args []string) *exec.Cmd {
if ss.srv.tailscaledPath == "" {
if ss.conn.srv.tailscaledPath == "" {
return exec.CommandContext(ctx, name, args...)
}
lu := ss.localUser
ci := ss.connInfo
lu := ss.conn.localUser
ci := ss.conn.info
remoteUser := ci.uprof.LoginName
if len(ci.node.Tags) > 0 {
remoteUser = strings.Join(ci.node.Tags, ",")
@@ -85,7 +85,7 @@ func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args
incubatorArgs = append(incubatorArgs, args...)
}
return exec.CommandContext(ctx, ss.srv.tailscaledPath, incubatorArgs...)
return exec.CommandContext(ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
}
const debugIncubator = false
@@ -166,7 +166,7 @@ func beIncubator(args []string) error {
//
// It sets ss.cmd, stdin, stdout, and stderr.
func (ss *sshSession) launchProcess(ctx context.Context) error {
shell := loginShell(ss.localUser.Uid)
shell := loginShell(ss.conn.localUser.Uid)
var args []string
if rawCmd := ss.RawCommand(); rawCmd != "" {
args = append(args, "-c", rawCmd)
@@ -174,10 +174,10 @@ func (ss *sshSession) launchProcess(ctx context.Context) error {
args = append(args, "-l") // login shell
}
ci := ss.connInfo
ci := ss.conn.info
cmd := ss.newIncubatorCommand(ctx, shell, args)
cmd.Dir = ss.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.localUser)...)
cmd.Dir = ss.conn.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
cmd.Env = append(cmd.Env, ss.Environ()...)
cmd.Env = append(cmd.Env,
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.IP(), ci.src.Port(), ci.dst.Port()),
@@ -330,7 +330,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
}
k, ok := opcodeShortName[c]
if !ok {
ss.logf("unknown opcode: %d", c)
ss.vlogf("unknown opcode: %d", c)
continue
}
if _, ok := tios.CC[k]; ok {
@@ -341,7 +341,7 @@ func (ss *sshSession) startWithPTY() (ptyFile *os.File, err error) {
tios.Opts[k] = v > 0
continue
}
ss.logf("unsupported opcode: %v(%d)=%v", k, c, v)
ss.vlogf("unsupported opcode: %v(%d)=%v", k, c, v)
}
// Save PTY settings.

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,19 @@ package tailssh
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"os/user"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
@@ -25,6 +31,7 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/util/cibuild"
"tailscale.com/util/lineread"
@@ -56,14 +63,17 @@ func TestMatchRule(t *testing.T) {
Action: someAction,
RuleExpires: timePtr(time.Unix(100, 0)),
},
ci: &sshConnInfo{now: time.Unix(200, 0)},
ci: &sshConnInfo{},
wantErr: errRuleExpired,
},
{
name: "no-principal",
rule: &tailcfg.SSHRule{
Action: someAction,
},
SSHUsers: map[string]string{
"*": "ubuntu",
}},
ci: &sshConnInfo{},
wantErr: errPrincipalMatch,
},
{
@@ -168,7 +178,11 @@ func TestMatchRule(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotUser, err := matchRule(tt.rule, tt.ci)
c := &conn{
now: time.Unix(200, 0),
info: tt.ci,
}
got, gotUser, err := c.matchRule(tt.rule, nil)
if err != tt.wantErr {
t.Errorf("err = %v; want %v", err, tt.wantErr)
}
@@ -205,17 +219,19 @@ func TestSSH(t *testing.T) {
lb: lb,
logf: logf,
}
ss, err := srv.newSSHServer()
sc, err := srv.newConn()
if err != nil {
t.Fatal(err)
}
// Remove the auth checks for the test
sc.insecureSkipTailscaleAuth = true
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
ci := &sshConnInfo{
sc.localUser = u
sc.info = &sshConnInfo{
sshUser: "test",
src: netaddr.MustParseIPPort("1.2.3.4:32342"),
dst: netaddr.MustParseIPPort("1.2.3.5:22"),
@@ -223,10 +239,8 @@ func TestSSH(t *testing.T) {
uprof: &tailcfg.UserProfile{},
}
ss.Handler = func(s ssh.Session) {
ss := srv.newSSHSession(s, ci, u)
ss.action = &tailcfg.SSHAction{Accept: true}
ss.run()
sc.Handler = func(s ssh.Session) {
sc.newSSHSession(s, &tailcfg.SSHAction{Accept: true}).run()
}
ln, err := net.Listen("tcp4", "127.0.0.1:0")
@@ -245,12 +259,13 @@ func TestSSH(t *testing.T) {
}
return
}
go ss.HandleConn(c)
go sc.HandleConn(c)
}
}()
execSSH := func(args ...string) *exec.Cmd {
cmd := exec.Command("ssh",
"-v",
"-p", fmt.Sprint(port),
"-o", "StrictHostKeyChecking=no",
"user@127.0.0.1")
@@ -266,7 +281,7 @@ func TestSSH(t *testing.T) {
cmd.Env = append(os.Environ(), "LOCAL_ENV=bar")
got, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err)
t.Fatal(err, string(got))
}
m := parseEnv(got)
if got := m["USER"]; got == "" || got != u.Username {
@@ -333,3 +348,86 @@ func parseEnv(out []byte) map[string]string {
})
return e
}
func TestPublicKeyFetching(t *testing.T) {
var reqsTotal, reqsIfNoneMatchHit, reqsIfNoneMatchMiss int32
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32((&reqsTotal), 1)
etag := fmt.Sprintf("W/%q", sha256.Sum256([]byte(r.URL.Path)))
w.Header().Set("Etag", etag)
if v := r.Header.Get("If-None-Match"); v != "" {
if v == etag {
atomic.AddInt32(&reqsIfNoneMatchHit, 1)
w.WriteHeader(304)
return
}
atomic.AddInt32(&reqsIfNoneMatchMiss, 1)
}
io.WriteString(w, "foo\nbar\n"+string(r.URL.Path)+"\n")
}))
ts.StartTLS()
defer ts.Close()
keys := ts.URL
clock := &tstest.Clock{}
srv := &server{
pubKeyHTTPClient: ts.Client(),
timeNow: clock.Now,
}
for i := 0; i < 2; i++ {
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
if err != nil {
t.Fatal(err)
}
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}
if got, want := atomic.LoadInt32(&reqsTotal), int32(1); got != want {
t.Errorf("got %d requests; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(0); got != want {
t.Errorf("got %d etag hits; want %d", got, want)
}
clock.Advance(5 * time.Minute)
got, err := srv.fetchPublicKeysURL(keys + "/alice.keys")
if err != nil {
t.Fatal(err)
}
if want := []string{"foo", "bar", "/alice.keys"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q; want %q", got, want)
}
if got, want := atomic.LoadInt32(&reqsTotal), int32(2); got != want {
t.Errorf("got %d requests; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchHit), int32(1); got != want {
t.Errorf("got %d etag hits; want %d", got, want)
}
if got, want := atomic.LoadInt32(&reqsIfNoneMatchMiss), int32(0); got != want {
t.Errorf("got %d etag misses; want %d", got, want)
}
}
func TestExpandPublicKeyURL(t *testing.T) {
c := &conn{
info: &sshConnInfo{
uprof: &tailcfg.UserProfile{
LoginName: "bar@baz.tld",
},
},
}
if got, want := c.expandPublicKeyURL("foo"), "foo"; got != want {
t.Errorf("basic: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/$LOGINNAME_LOCALPART.keys"), "https://example.com/bar.keys"; got != want {
t.Errorf("localpart: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email=bar@baz.tld"; got != want {
t.Errorf("email: got %q; want %q", got, want)
}
c.info = new(sshConnInfo)
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email="; got != want {
t.Errorf("on empty: got %q; want %q", got, want)
}
}

View File

@@ -67,8 +67,9 @@ type CapabilityVersion int
// 28: 2022-03-09: client can communicate over Noise.
// 29: 2022-03-21: MapResponse.PopBrowserURL
// 30: 2022-03-22: client can request id tokens.
// 31: 2022-04-15: PingRequest TSMP support
const CurrentCapabilityVersion CapabilityVersion = 31
// 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
// 32: 2022-04-17: client knows FilterRule.CapMatch
const CurrentCapabilityVersion CapabilityVersion = 32
type StableID string
@@ -459,6 +460,7 @@ type Hostinfo struct {
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
OS string `json:",omitempty"` // operating system the client runs on (a version.OS value)
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
Desktop opt.Bool `json:",omitempty"` // if a desktop was detected on Linux
Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
Hostname string `json:",omitempty"` // name of the host the client runs on
@@ -1051,6 +1053,18 @@ type NetPortRange struct {
Ports PortRange
}
// CapGrant grants capabilities in a FilterRule.
type CapGrant struct {
// Dsts are the destination IP ranges that this capabilty
// grant matches.
Dsts []netaddr.IPPrefix
// Caps are the capabilities the source IP matched by
// FilterRule.SrcIPs are granted to the destination IP,
// matched by Dsts.
Caps []string `json:",omitempty"`
}
// FilterRule represents one rule in a packet filter.
//
// A rule is logically a set of source CIDRs to match (described by
@@ -1081,7 +1095,9 @@ type FilterRule struct {
// DstPorts are the port ranges to allow once a source IP
// matches (is in the CIDR described by SrcIPs & SrcBits).
DstPorts []NetPortRange
//
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
DstPorts []NetPortRange `json:",omitempty"`
// IPProto are the IP protocol numbers to match.
//
@@ -1093,6 +1109,18 @@ type FilterRule struct {
// Depending on the IPProto values, DstPorts may or may not be
// used.
IPProto []int `json:",omitempty"`
// CapGrant, if non-empty, are the capabilities to
// conditionally grant to the source IP in SrcIPs.
//
// Think of DstPorts as "capabilities for networking" and
// CapGrant as arbitrary application-defined capabilities
// defined between the admin's ACLs and the application
// doing WhoIs lookups, looking up the remote IP address's
// application-level capabilities.
//
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
CapGrant []CapGrant `json:",omitempty"`
}
var FilterAllowAll = []FilterRule{
@@ -1195,8 +1223,8 @@ type DNSRecord struct {
// PingRequest with no IP and Types is a request to send an HTTP request to prove the
// long-polling client is still connected.
// PingRequest with Types and IP, will send a ping to the IP and send a
// POST request to the URL to prove that the ping succeeded.
// PingRequest with Types and IP, will send a ping to the IP and send a POST
// request containing a PingResponse to the URL containing results.
type PingRequest struct {
// URL is the URL to send a HEAD request to.
// It will be a unique URL each time. No auth headers are necessary.
@@ -1219,6 +1247,48 @@ type PingRequest struct {
IP netaddr.IP
}
// PingResponse provides result information for a TSMP or Disco PingRequest.
// Typically populated from an ipnstate.PingResult used in `tailscale ping`.
type PingResponse struct {
Type string // ping type, such as TSMP or disco.
IP string `json:",omitempty"` // ping destination
NodeIP string `json:",omitempty"` // Tailscale IP of node handling IP (different for subnet routers)
NodeName string `json:",omitempty"` // DNS name base or (possibly not unique) hostname
// Err contains a short description of error conditions if the PingRequest
// could not be fulfilled for some reason.
// e.g. "100.1.2.3 is local Tailscale IP"
Err string `json:",omitempty"`
// LatencySeconds reports measurement of the round-trip time of a message to
// the requested target, if it could be determined. If LatencySeconds is
// omitted, Err should contain information as to the cause.
LatencySeconds float64 `json:",omitempty"`
// Endpoint is the ip:port if direct UDP was used.
// It is not currently set for TSMP pings.
Endpoint string `json:",omitempty"`
// DERPRegionID is non-zero DERP region ID if DERP was used.
// It is not currently set for TSMP pings.
DERPRegionID int `json:",omitempty"`
// DERPRegionCode is the three-letter region code
// corresponding to DERPRegionID.
// It is not currently set for TSMP pings.
DERPRegionCode string `json:",omitempty"`
// PeerAPIPort is set by TSMP ping responses for peers that
// are running a peerapi server. This is the port they're
// running the server on.
PeerAPIPort uint16 `json:",omitempty"`
// IsLocalIP is whether the ping request error is due to it being
// a ping to the local node.
IsLocalIP bool `json:",omitempty"`
}
type MapResponse struct {
// KeepAlive, if set, represents an empty message just to keep
// the connection alive. When true, all other fields except
@@ -1389,6 +1459,10 @@ type Debug struct {
// new attempts at UPnP connections.
DisableUPnP opt.Bool `json:",omitempty"`
// DisableLogTail disables the logtail package. Once disabled it can't be
// re-enabled for the lifetime of the process.
DisableLogTail bool `json:",omitempty"`
// Exit optionally specifies that the client should os.Exit
// with this code.
Exit *int `json:",omitempty"`
@@ -1508,8 +1582,19 @@ type Oauth2Token struct {
}
const (
// MapResponse.Node self capabilities.
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
// Inter-node capabilities.
// CapabilityFileSharingSend grants the ability to receive files from a
// node that's owned by a different user.
CapabilityFileSharingSend = "https://tailscale.com/cap/file-send"
// CapabilityDebugPeer grants the ability for a peer to read this node's
// goroutines, metrics, magicsock internal state, etc.
CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
)
// SetDNSRequest is a request to add a DNS record.
@@ -1551,9 +1636,21 @@ type SetDNSResponse struct{}
// SSHPolicy is the policy for how to handle incoming SSH connections
// over Tailscale.
type SSHPolicy struct {
// Rules are the rules to process for an incoming SSH
// connection. The first matching rule takes its action and
// stops processing further rules.
// Rules are the rules to process for an incoming SSH connection. The first
// matching rule takes its action and stops processing further rules.
//
// When an incoming connection first starts, all rules are evaluated in
// "none" auth mode, where the client hasn't even been asked to send a
// public key. All SSHRule.Principals requiring a public key won't match. If
// a rule matches on the first pass and its Action is reject, the
// authentication fails with that action's rejection message, if any.
//
// If the first pass rule evaluation matches nothing without matching an
// Action with Reject set, the rules are considered to see whether public
// keys might still result in a match. If not, "none" auth is terminated
// before proceeding to public key mode. If so, the client is asked to try
// public key authentication and the rules are evaluated again for each of
// the client's present keys.
Rules []*SSHRule `json:"rules"`
}
@@ -1594,16 +1691,26 @@ type SSHRule struct {
}
// SSHPrincipal is either a particular node or a user on any node.
// Any matching field causes a match.
type SSHPrincipal struct {
// Matching any one of the following four field causes a match.
// It must also match Certs, if non-empty.
Node StableNodeID `json:"node,omitempty"`
NodeIP string `json:"nodeIP,omitempty"`
UserLogin string `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
// Any, if true, matches any user.
Any bool `json:"any,omitempty"`
Any bool `json:"any,omitempty"` // if true, match any connection
// TODO(bradfitz): add StableUserID, once that exists
// PubKeys, if non-empty, means that this SSHPrincipal only
// matches if one of these public keys is presented by the user.
//
// As a special case, if len(PubKeys) == 1 and PubKeys[0] starts
// with "https://", then it's fetched (like https://github.com/username.keys).
// In that case, the following variable expansions are also supported
// in the URL:
// * $LOGINNAME_EMAIL ("foo@bar.com" or "foo@github")
// * $LOGINNAME_LOCALPART (the "foo" from either of the above)
PubKeys []string `json:"pubKeys,omitempty"`
}
// SSHAction is how to handle an incoming connection.

View File

@@ -117,6 +117,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
BackendLogID string
OS string
OSVersion string
Desktop opt.Bool
Package string
DeviceModel string
Hostname string

View File

@@ -28,7 +28,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestHostinfoEqual(t *testing.T) {
hiHandles := []string{
"IPNVersion", "FrontendLogID", "BackendLogID",
"OS", "OSVersion", "Package", "DeviceModel", "Hostname",
"OS", "OSVersion", "Desktop", "Package", "DeviceModel", "Hostname",
"ShieldsUp", "ShareeNode",
"GoArch",
"RoutableIPs", "RequestTags",

View File

@@ -1,6 +1,7 @@
package ssh_test
import (
"errors"
"io"
"io/ioutil"
@@ -27,10 +28,19 @@ func ExampleNoPty() {
func ExamplePublicKeyAuth() {
ssh.ListenAndServe(":2222", nil,
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
data, _ := ioutil.ReadFile("/path/to/allowed/key.pub")
allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data)
return ssh.KeysEqual(key, allowed)
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error {
data, err := ioutil.ReadFile("/path/to/allowed/key.pub")
if err != nil {
return err
}
allowed, _, _, _, err := ssh.ParseAuthorizedKey(data)
if err != nil {
return err
}
if !ssh.KeysEqual(key, allowed) {
return errors.New("some error")
}
return nil
}),
)
}

View File

@@ -38,8 +38,6 @@ type Server struct {
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
NoClientAuthCallback func(gossh.ConnMetadata) (*gossh.Permissions, error)
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
@@ -131,10 +129,6 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
config.NoClientAuth = true
}
if srv.NoClientAuthCallback != nil {
config.NoClientAuth = true
config.NoClientAuthCallback = srv.NoClientAuthCallback
}
if srv.Version != "" {
config.ServerVersion = "SSH-2.0-" + srv.Version
}
@@ -150,8 +144,8 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
if srv.PublicKeyHandler != nil {
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
applyConnMetadata(ctx, conn)
if ok := srv.PublicKeyHandler(ctx, key); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
if err := srv.PublicKeyHandler(ctx, key); err != nil {
return ctx.Permissions().Permissions, err
}
ctx.SetValue(ContextKeyPublicKey, key)
return ctx.Permissions().Permissions, nil

View File

@@ -36,7 +36,7 @@ type Option func(*Server) error
type Handler func(Session)
// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) bool
type PublicKeyHandler func(ctx Context, key PublicKey) error
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool

View File

@@ -33,6 +33,7 @@ import (
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"
_ "tailscale.com/safesocket"
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb"
_ "tailscale.com/types/flagtype"

View File

@@ -33,6 +33,7 @@ import (
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"
_ "tailscale.com/safesocket"
_ "tailscale.com/ssh/tailssh"
_ "tailscale.com/tailcfg"
_ "tailscale.com/tsweb"
_ "tailscale.com/types/flagtype"

62
tstest/iosdeps/iosdeps.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package iosdeps is a just a list of the packages we import on iOS, to let us
// test that our transitive closure of dependencies on iOS doesn't accidentally
// grow too large, as we've historically been memory constrained there.
package iosdeps
import (
_ "bufio"
_ "bytes"
_ "context"
_ "crypto/rand"
_ "crypto/sha256"
_ "encoding/json"
_ "errors"
_ "fmt"
_ "io"
_ "io/fs"
_ "io/ioutil"
_ "log"
_ "math"
_ "net"
_ "net/http"
_ "os"
_ "os/signal"
_ "path/filepath"
_ "runtime"
_ "runtime/debug"
_ "strings"
_ "sync"
_ "sync/atomic"
_ "syscall"
_ "time"
_ "unsafe"
_ "go4.org/mem"
_ "golang.org/x/sys/unix"
_ "golang.zx2c4.com/wireguard/device"
_ "golang.zx2c4.com/wireguard/tun"
_ "inet.af/netaddr"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/localapi"
_ "tailscale.com/log/logheap"
_ "tailscale.com/logtail"
_ "tailscale.com/logtail/filch"
_ "tailscale.com/net/dns"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"
_ "tailscale.com/tempfork/pprof"
_ "tailscale.com/types/empty"
_ "tailscale.com/types/logger"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/dnsname"
_ "tailscale.com/version"
_ "tailscale.com/wgengine"
_ "tailscale.com/wgengine/router"
)

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// No need to run this on Windows where CI's slow enough. Then we don't need to
// worry about "go.exe" etc.
//go:build !windows
// +build !windows
package iosdeps
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestDeps(t *testing.T) {
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "list", "-json", ".")
cmd.Env = append(os.Environ(), "GOOS=ios", "GOARCH=arm64")
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
var res struct {
Deps []string
}
if err := json.Unmarshal(out, &res); err != nil {
t.Fatal(err)
}
for _, dep := range res.Deps {
switch dep {
case "regexp", "regexp/syntax", "text/template", "html/template":
t.Errorf("package %q is not allowed as a dependency on iOS", dep)
}
}
t.Logf("got %d dependencies", len(res.Deps))
}

View File

@@ -12,7 +12,9 @@ import "inet.af/netaddr"
// Resolver is the configuration for one DNS resolver.
type Resolver struct {
// Addr is the address of the DNS resolver, one of:
// - A plain IP address for a "classic" UDP+TCP DNS resolver
// - A plain IP address for a "classic" UDP+TCP DNS resolver.
// This is the common format as sent by the control plane.
// - An IP:port, for tests.
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
// - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS
Addr string `json:",omitempty"`
@@ -26,7 +28,20 @@ type Resolver struct {
BootstrapResolution []netaddr.IP `json:",omitempty"`
}
// ResolverFromIP defines a Resolver for ip on port 53.
func ResolverFromIP(ip netaddr.IP) Resolver {
return Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()}
// IPPort returns r.Addr as an IP address and port if either
// r.Addr is an IP address (the common case) or if r.Addr
// is an IP:port (as done in tests).
func (r *Resolver) IPPort() (ipp netaddr.IPPort, ok bool) {
if r.Addr == "" || r.Addr[0] == 'h' || r.Addr[0] == 't' {
// Fast path to avoid ParseIP error allocation for obviously not IP
// cases.
return
}
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
return netaddr.IPPortFrom(ip, 53), true
}
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil {
return ipp, true
}
return
}

View File

@@ -27,10 +27,12 @@ type Filter struct {
// destination within local, regardless of the policy filter
// below.
local *netaddr.IPSet
// logIPs is the set of IPs that are allowed to appear in flow
// logs. If a packet is to or from an IP not in logIPs, it will
// never be logged.
logIPs *netaddr.IPSet
// matches4 and matches6 are lists of match->action rules
// applied to all packets arriving over tailscale
// tunnels. Matches are checked in order, and processing stops
@@ -38,6 +40,11 @@ type Filter struct {
// match is to drop the packet.
matches4 matches
matches6 matches
// cap4 and cap6 are the subsets of the matches that are about
// capability grants, partitioned by source IP address family.
cap4, cap6 matches
// state is the connection tracking state attached to this
// filter. It is used to allow incoming traffic that is a response
// to an outbound connection that this node made, even if those
@@ -174,6 +181,8 @@ func New(matches []Match, localNets *netaddr.IPSet, logIPs *netaddr.IPSet, share
logf: logf,
matches4: matchesFamily(matches, netaddr.IP.Is4),
matches6: matchesFamily(matches, netaddr.IP.Is6),
cap4: capMatchesFunc(matches, netaddr.IP.Is4),
cap6: capMatchesFunc(matches, netaddr.IP.Is6),
local: localNets,
logIPs: logIPs,
state: state,
@@ -205,6 +214,27 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
return ret
}
// capMatchesFunc returns a copy of the subset of ms for which keep(srcNet.IP)
// and the match is a capability grant.
func capMatchesFunc(ms matches, keep func(netaddr.IP) bool) matches {
var ret matches
for _, m := range ms {
if len(m.Caps) == 0 {
continue
}
retm := Match{Caps: m.Caps}
for _, src := range m.Srcs {
if keep(src.IP()) {
retm.Srcs = append(retm.Srcs, src)
}
}
if len(retm.Srcs) > 0 {
ret = append(ret, retm)
}
}
return ret
}
func maybeHexdump(flag RunFlags, b []byte) string {
if flag == 0 {
return ""
@@ -291,6 +321,30 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
return f.RunIn(pkt, 0)
}
// AppendCaps appends to base the capabilities that srcIP has talking
// to dstIP.
func (f *Filter) AppendCaps(base []string, srcIP, dstIP netaddr.IP) []string {
ret := base
var mm matches
switch {
case srcIP.Is4():
mm = f.cap4
case srcIP.Is6():
mm = f.cap6
}
for _, m := range mm {
if !ipInList(srcIP, m.Srcs) {
continue
}
for _, cm := range m.Caps {
if cm.Cap != "" && cm.Dst.Contains(dstIP) {
ret = append(ret, cm.Cap)
}
}
}
return ret
}
// ShieldsUp reports whether this is a "shields up" (block everything
// incoming) filter.
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }

View File

@@ -872,3 +872,83 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
})
}
}
func TestCaps(t *testing.T) {
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
{
SrcIPs: []string{"*"},
CapGrant: []tailcfg.CapGrant{{
Dsts: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
},
Caps: []string{"is_ipv4"},
}},
},
{
SrcIPs: []string{"*"},
CapGrant: []tailcfg.CapGrant{{
Dsts: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("::/0"),
},
Caps: []string{"is_ipv6"},
}},
},
{
SrcIPs: []string{"100.199.0.0/16"},
CapGrant: []tailcfg.CapGrant{{
Dsts: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.200.0.0/16"),
},
Caps: []string{"some_super_admin"},
}},
},
})
if err != nil {
t.Fatal(err)
}
filt := New(mm, nil, nil, nil, t.Logf)
tests := []struct {
name string
src, dst string // IP
want []string
}{
{
name: "v4",
src: "1.2.3.4",
dst: "2.4.5.5",
want: []string{"is_ipv4"},
},
{
name: "v6",
src: "1::1",
dst: "2::2",
want: []string{"is_ipv6"},
},
{
name: "admin",
src: "100.199.1.2",
dst: "100.200.3.4",
want: []string{"is_ipv4", "some_super_admin"},
},
{
name: "not_admin_bad_src",
src: "100.198.1.2", // 198, not 199
dst: "100.200.3.4",
want: []string{"is_ipv4"},
},
{
name: "not_admin_bad_dst",
src: "100.199.1.2",
dst: "100.201.3.4", // 201, not 200
want: []string{"is_ipv4"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filt.AppendCaps(nil, netaddr.MustParseIP(tt.src), netaddr.MustParseIP(tt.dst))
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}

View File

@@ -47,12 +47,24 @@ func (npr NetPortRange) String() string {
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
}
// CapMatch is a capability grant match predicate.
type CapMatch struct {
// Dst is the IP prefix that the destination IP address matches against
// to get the capability.
Dst netaddr.IPPrefix
// Cap is the capability that's granted if the destination IP addresses
// matches Dst.
Cap string
}
// Match matches packets from any IP address in Srcs to any ip:port in
// Dsts.
type Match struct {
IPProto []ipproto.Proto // required set (no default value at this layer)
Dsts []NetPortRange
Srcs []netaddr.IPPrefix
Dsts []NetPortRange // optional, if Srcs match
Caps []CapMatch // optional, if Srcs match
}
func (m Match) String() string {

View File

@@ -21,14 +21,16 @@ func (src *Match) Clone() *Match {
dst := new(Match)
*dst = *src
dst.IPProto = append(src.IPProto[:0:0], src.IPProto...)
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
dst.Caps = append(src.Caps[:0:0], src.Caps...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _MatchCloneNeedsRegeneration = Match(struct {
IPProto []ipproto.Proto
Dsts []NetPortRange
Srcs []netaddr.IPPrefix
Dsts []NetPortRange
Caps []CapMatch
}{})

View File

@@ -70,6 +70,16 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
})
}
}
for _, cm := range r.CapGrant {
for _, dstNet := range cm.Dsts {
for _, cap := range cm.Caps {
m.Caps = append(m.Caps, CapMatch{
Dst: dstNet,
Cap: cap,
})
}
}
}
mm = append(mm, m)
}

View File

@@ -42,6 +42,10 @@ type osMon interface {
// until the osMon is closed. After a Close, the returned
// error is ignored.
Receive() (message, error)
// IsInterestingInterface reports whether the provided interface should
// be considered for network change events.
IsInterestingInterface(iface string) bool
}
// ChangeFunc is a callback function that's called when the network
@@ -185,13 +189,7 @@ func (m *Mon) Start() {
}
m.started = true
switch runtime.GOOS {
case "ios", "android":
// For battery reasons, and because these platforms
// don't really sleep in the same way, don't poll
// for the wall time to detect for wake-for-sleep
// walltime jumps.
default:
if shouldMonitorTimeJump {
m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime)
}
@@ -288,6 +286,13 @@ func (m *Mon) notifyRuleDeleted(rdm ipRuleDeletedMessage) {
}
}
// isInterestingInterface reports whether the provided interface should be
// considered when checking for network state changes.
// The ips parameter should be the IPs of the provided interface.
func (m *Mon) isInterestingInterface(i interfaces.Interface, ips []netaddr.IPPrefix) bool {
return m.om.IsInterestingInterface(i.Name) && interfaces.UseInterestingInterfaces(i, ips)
}
// debounce calls the callback function with a delay between events
// and exits when a stop is issued.
func (m *Mon) debounce() {
@@ -304,27 +309,26 @@ func (m *Mon) debounce() {
} else {
m.mu.Lock()
// See if we have a queued or new time jump signal.
m.checkWallTimeAdvanceLocked()
timeJumped := m.timeJumped
if timeJumped {
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
}
oldState := m.ifState
ifChanged := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs)
if ifChanged {
changed := !curState.EqualFiltered(oldState, m.isInterestingInterface, interfaces.UseInterestingIPs)
if changed {
m.gwValid = false
m.ifState = curState
if s1, s2 := oldState.String(), curState.String(); s1 == s2 {
m.logf("[unexpected] network state changed, but stringification didn't: %v\nold: %s\nnew: %s\n", s1,
jsonSummary(oldState), jsonSummary(curState))
m.logf("[unexpected] network state changed, but stringification didn't: %v", s1)
m.logf("[unexpected] old: %s", jsonSummary(oldState))
m.logf("[unexpected] new: %s", jsonSummary(curState))
}
}
changed := ifChanged || timeJumped
if changed {
m.timeJumped = false
// See if we have a queued or new time jump signal.
if shouldMonitorTimeJump && m.checkWallTimeAdvanceLocked() {
m.resetTimeJumpedLocked()
if !changed {
// Only log if it wasn't an interesting change.
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
changed = true
}
}
for _, cb := range m.cbs {
go cb(changed, m.ifState)
@@ -360,22 +364,37 @@ func (m *Mon) pollWallTime() {
if m.closed {
return
}
m.checkWallTimeAdvanceLocked()
if m.timeJumped {
if m.checkWallTimeAdvanceLocked() {
m.InjectEvent()
}
m.wallTimer.Reset(pollWallTimeInterval)
}
// checkWallTimeAdvanceLocked updates m.timeJumped, if wall time jumped
// more than 150% of pollWallTimeInterval, indicating we probably just
// came out of sleep.
func (m *Mon) checkWallTimeAdvanceLocked() {
// shouldMonitorTimeJump is whether we keep a regular periodic timer running in
// the background watching for jumps in wall time.
//
// We don't do this on mobile platforms for battery reasons, and because these
// platforms don't really sleep in the same way.
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios"
// checkWallTimeAdvanceLocked reports whether wall time jumped more than 150% of
// pollWallTimeInterval, indicating we probably just came out of sleep. Once a
// time jump is detected it must be reset by calling resetTimeJumpedLocked.
func (m *Mon) checkWallTimeAdvanceLocked() bool {
if !shouldMonitorTimeJump {
panic("unreachable") // if callers are correct
}
now := wallTime()
if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 {
m.timeJumped = true
m.timeJumped = true // it is reset by debounce.
}
m.lastWall = now
return m.timeJumped
}
// resetTimeJumpedLocked consumes the signal set by checkWallTimeAdvanceLocked.
func (m *Mon) resetTimeJumpedLocked() {
m.timeJumped = false
}
type ipRuleDeletedMessage struct {

View File

@@ -112,11 +112,18 @@ func addrType(addrs []route.Addr, rtaxType int) route.Addr {
return nil
}
func (m *darwinRouteMon) IsInterestingInterface(iface string) bool {
baseName := strings.TrimRight(iface, "0123456789")
switch baseName {
case "llw", "awdl", "pdp_ip", "ipsec":
return false
}
return true
}
func (m *darwinRouteMon) skipInterfaceAddrMessage(msg *route.InterfaceAddrMessage) bool {
if la, ok := addrType(msg.Addrs, unix.RTAX_IFP).(*route.LinkAddr); ok {
baseName := strings.TrimRight(la.Name, "0123456789")
switch baseName {
case "llw", "awdl", "pdp_ip", "ipsec":
if !m.IsInterestingInterface(la.Name) {
return true
}
}

View File

@@ -34,6 +34,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
return &devdConn{conn}, nil
}
func (c *devdConn) IsInterestingInterface(iface string) bool { return true }
func (c *devdConn) Close() error {
return c.conn.Close()
}

View File

@@ -64,6 +64,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
return &nlConn{logf: logf, conn: conn, addrCache: make(map[uint32]map[netaddr.IP]bool)}, nil
}
func (c *nlConn) IsInterestingInterface(iface string) bool { return true }
func (c *nlConn) Close() error { return c.conn.Close() }
func (c *nlConn) Receive() (message, error) {

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 !android
// +build !android
package monitor
import (

View File

@@ -72,6 +72,8 @@ func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) {
return m, nil
}
func (m *winMon) IsInterestingInterface(iface string) bool { return true }
func (m *winMon) Close() (ret error) {
m.cancel()
m.noDeadlockTicker.Stop()

View File

@@ -38,6 +38,10 @@ type pollingMon struct {
stop chan struct{}
}
func (pm *pollingMon) IsInterestingInterface(iface string) bool {
return true
}
func (pm *pollingMon) Close() error {
pm.closeOnce.Do(func() {
close(pm.stop)

View File

@@ -662,12 +662,9 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
c := gonet.NewTCPConn(&wq, ep)
if ns.lb != nil {
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) && handleSSH != nil {
ns.logf("handling SSH connection....")
if err := handleSSH(ns.logf, ns.lb, c); err != nil {
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) {
if err := ns.lb.HandleSSHConn(c); err != nil {
ns.logf("ssh error: %v", err)
} else {
ns.logf("ssh: ok")
}
return
}

View File

@@ -1,14 +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.
//go:build linux || (darwin && !ios)
// +build linux darwin,!ios
package netstack
import "tailscale.com/ssh/tailssh"
func init() {
handleSSH = tailssh.Handle
}

View File

@@ -1514,8 +1514,14 @@ func supportsV6NAT() bool {
// Can't read the file. Assume SNAT works.
return true
}
return bytes.Contains(bs, []byte("nat\n"))
if bytes.Contains(bs, []byte("nat\n")) {
return true
}
// In nftables mode, that proc file will be empty. Try another thing:
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
return true
}
return false
}
func checkIPRuleSupportsV6(logf logger.Logf) error {