Compare commits

...

78 Commits

Author SHA1 Message Date
Brad Fitzpatrick
8f8ce6685e WIP
Change-Id: Ia15bca4af3f639b6f1fc0fc9950a065c74841b77
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 12:34:02 -07:00
Maisem Ali
4899c2c1f4 cmd/containerboot: revert to using tailscale up
This partially reverts commits a61a9ab087
and 7538f38671 and fully reverts
4823a7e591.

The goal of that commit was to reapply known config whenever the
container restarts. However, that already happens when TS_AUTH_ONCE was
false (the default back then). So we only had to selectively reapply the
config if TS_AUTH_ONCE is true, this does exactly that.

This is a little sad that we have to revert to `tailscale up`, but it
fixes the backwards incompatibility problem.

Updates tailscale/tailscale#9539

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-16 12:00:44 -07:00
Andrew Lytvynov
b949e208bb ipn/ipnlocal: fix AllowsUpdate disable after enable (#9827)
The old code would always retain value `true` if it was set once, even
if you then change `prefs.AutoUpdate.Apply` to `false`.
Instead of using the previous value, use the default (envknob) value to
OR with.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-16 10:54:56 -07:00
Brad Fitzpatrick
18bd98d35b cmd/tailscaled,*: add start of configuration file support
Updates #1412

Co-authored-by: Maisem Ali <maisem@tailscale.com>
Change-Id: I38d559c1784d09fc804f521986c9b4b548718f7d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 10:40:27 -07:00
Rhea Ghosh
71271e41d6 ipn/{ipnlocal/peerapi, localapi} initial taildrop resume api plumbing (#9798)
This change:
* adds a partial files peerAPI endpoint to get a list of partial files
* adds a helper function to extract the basename of a file
* updates the peer put peerAPI endpoint
* updates the file put localapi endpoint to allow resume functionality

Updates #14772

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-16 12:36:31 -05:00
Brad Fitzpatrick
95faefd1f6 net/dnsfallback: disable recursive resolver for now
It seems to be implicated in a CPU consumption bug that's not yet
understood. Disable it until we understand.

Updates tailscale/corp#15261

Change-Id: Ia6d0c310da6464dda79a70fc3c18be0782812d3f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 09:47:08 -07:00
Andrew Lytvynov
8a5b02133d clientupdate: return ErrUnsupported for macSys clients (#9793)
The Sparkle-based update is not quite working yet. Make `NewUpdater`
return `ErrUnsupported` for it to avoid the proliferation of exceptions
up the stack.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-16 09:14:14 -07:00
Flakes Updater
51078b6486 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-16 09:12:16 -07:00
Brad Fitzpatrick
7fd6cc3caa go.mod: bump alexbrainman/sspi
For https://github.com/alexbrainman/sspi/pull/13

Fixes #9131 (hopefully)

Change-Id: I27bb00bbf5e03850f65f18c45f15c4441cc54b23
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-16 09:10:27 -07:00
Sonia Appasamy
feabb34ea0 ipn/localapi: add debug-web-client endpoint
Debug endpoint for the web client's auth flow to talk back to the
control server. Restricted behind a feature flag on control.

We will either be removing this debug endpoint, or renaming it
before launching the web client updates.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-16 10:52:23 -04:00
Kristoffer Dalby
e06f2f1873 ipn/ipnlocal: change serial number policy to be PreferenceOption
This commit changes the PostureChecking syspolicy key to be a
PreferenceOption(user-defined, always, never) instead of Bool.

This aligns better with the defaults implementation on macOS allowing
CLI arguments to be read when user-defined or no defaults is set.

Updates #tailscale/tailscale/5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-16 16:01:54 +02:00
Denton Gentry
97ee3891f1 net/dns: use direct when NetworkManager has no systemd-resolved
Endeavour OS, at least, uses NetworkManager 1.44.2 and does
not use systemd-resolved behind the scenes at all. If we
find ourselves in that situation, return "direct" not
"systemd-resolved"

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-10-15 17:12:49 -07:00
Brad Fitzpatrick
56ebcd1ed4 .github/workflows: break up race builder a bit more
Move the compilation of everything to its own job too, separate
from test execution.

Updates #7894

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 19:28:31 -07:00
Brad Fitzpatrick
e89927de2b tsnet: fix data race in TestFallbackTCPHandler
Fixes #9805

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 19:12:43 -07:00
Brad Fitzpatrick
18e2936d25 github/workflows: move race tests to their own job
They're slow. Make them their own job that can run in parallel.

Also, only run them in race mode. No need to run them on 386
or non-race amd64.

Updates #7894

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 14:08:58 -07:00
Brad Fitzpatrick
c363b9055d tstest/integration: add tests for tun mode (requiring root)
Updates #7894

Change-Id: Iff0b07b21ae28c712dd665b12918fa28d6f601d0
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 13:52:30 -07:00
Brad Fitzpatrick
a6270826a3 wgengine/magicsock: fix data race regression in disco ping callbacks
Regression from c15997511d. The callback could be run multiple times
from different endpoints.

Fixes #9801

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-14 13:52:30 -07:00
Maisem Ali
5297bd2cff cmd/tailscaled,net/tstun: fix data race on start-up in TUN mode
Fixes #7894

Change-Id: Ice3f8019405714dd69d02bc07694f3872bb598b8

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-14 08:54:30 -07:00
Brad Fitzpatrick
5c555cdcbb tstest/integration: set race flag when cross compiling, conditionally fail on race
Misc cleanups and things noticed while working on #7894 and pulled out
of a separate change. Submitting them on their own to not distract
from later changes.

Updates #7894

Change-Id: Ie9abc8b88f121c559aeeb7e74db2aa532eb84d3d
Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-13 20:01:32 -07:00
Rhea Ghosh
8c7169105e ipn/{ipnlocal/peerapi, localapi}: cleaning up http statuses for consistency and readability (#9796)
Updates #cleanup

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-13 17:40:10 -05:00
Joe Tsai
9cb6c5bb78 util/httphdr: add new package for parsing HTTP headers (#9797)
This adds support for parsing Range and Content-Range headers
according to RFC 7230. The package could be extended in the future
to handle other headers.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-13 15:38:22 -07:00
Andrew Lytvynov
af5a586463 ipn/ipnlocal: include AutoUpdate prefs in HostInfo.AllowsUpdate (#9792)
Updates #9260

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-13 11:14:23 -07:00
Claire Wang
754fb9a8a8 tailcfg: add tailnet field to register request (#9675)
Updates tailscale/corp#10967

Signed-off-by: Claire Wang <claire@tailscale.com>
2023-10-13 14:13:41 -04:00
Joe Tsai
8f948638c5 taildrop: minor cleanups and fixes (#9786)
Perform the same m==nil check in Manager.{PartialFiles,HashPartialFile}
as we do in the other methods.

Fix HashPartialFile is properly handle a length of -1.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-13 10:21:15 -05:00
Joe Tsai
b1867eb23f taildrop: add logic for resuming partial files (#9785)
We add the following API:
* type FileChecksums
* type Checksum
* func Manager.PartialFiles
* func Manager.HashPartialFile
* func ResumeReader

The Manager methods provide the ability to query for partial files
and retrieve a list of checksums for a given partial file.
The ResumeReader function is a helper that wraps an io.Reader
to discard content that is identical locally and remotely.
The FileChecksums type represents the checksums of a file
and is safe to JSON marshal and send over the wire.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-12 16:50:11 -07:00
Maisem Ali
24f322bc43 ipn/ipnlocal: do unexpired cert renewals in the background
We were eagerly doing a synchronous renewal of the cert while
trying to serve traffic. Instead of that, just do the cert
renewal in the background and continue serving traffic as long
as the cert is still valid.

This regressed in c1ecae13ab when
we introduced ARI support and were trying to make the experience
of `tailscale cert` better. However, that ended up regressing
the experience for tsnet as it would not always doing the renewal
synchronously.

Fixes #9783

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-12 16:02:45 -07:00
Joe Tsai
1a78f240b5 tstime: add DefaultClock (#9691)
In almost every single use of Clock, there is a default behavior
we want to use when the interface is nil,
which is to use the the standard time package.

The Clock interface exists only for testing,
and so tests that care about mocking time
can adequately plumb the the Clock down the stack
and through various data structures.

However, the problem with Clock is that there are many
situations where we really don't care about mocking time
(e.g., measuring execution time for a log message),
where making sure that Clock is non-nil is not worth the burden.
In fact, in a recent refactoring, the biggest pain point was
dealing with nil-interface panics when calling tstime.Clock methods
where mocking time wasn't even needed for the relevant tests.
This required wasted time carefully reviewing the code to
make sure that tstime.Clock was always populated,
and even then we're not statically guaranteed to avoid a nil panic.

Ideally, what we want are default methods on Go interfaces,
but such a language construct does not exist.
However, we can emulate that behavior by declaring
a concrete type that embeds the interface.
If the underlying interface value is nil,
it provides some default behavior (i.e., use StdClock).

This provides us a nice balance of two goals:
* We can plumb tstime.DefaultClock in all relevant places
  for use with mocking time in the tests that care.
* For all other logic that don't care about,
  we never need to worry about whether tstime.DefaultClock
  is nil or not. This is especially relevant in production code
  where we don't want to panic.

Longer-term, we may want to perform a large-scale change
where we rename Clock to ClockInterface
and rename DefaultClock to just Clock.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-12 16:01:17 -07:00
Naman Sood
7783a960e8 client/web: add metric for exit node advertising (#9781)
* client/web: add metric for exit node advertising

Updates tailscale/corp#15215

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

* client/web: use http request's context for IncrementCounter

Updates #cleanup

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

---------

Signed-off-by: Naman Sood <mail@nsood.in>
2023-10-12 17:02:20 -04:00
James Tucker
ce0830837d appctype: introduce a configuration schema for app connectors
Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-12 10:49:23 -07:00
Joe Tsai
37c646d9d3 taildrop: improve the functionality and reliability of put (#9762)
Changes made:
* Move all HTTP related functionality from taildrop to ipnlocal.
* Add two arguments to taildrop.Manager.PutFile to specify
  an opaque client ID and a resume offset (both unused for now).
* Cleanup the logic of taildrop.Manager.PutFile
  to be easier to follow.
* Implement file conflict handling where duplicate files are renamed
  (e.g., "IMG_1234.jpg" -> "IMG_1234 (2).jpg").
* Implement file de-duplication where "renaming" a partial file
  simply deletes it if it already exists with the same contents.
* Detect conflicting active puts where a second concurrent put
  results in an error.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
2023-10-12 09:28:46 -07:00
Maisem Ali
1294b89792 cmd/k8s-operator: allow setting same host value for tls and ingress rules
We were too strict and required the user not specify the host field at all
in the ingress rules, but that degrades compatibility with existing helm charts.

Relax the constraint so that rule.Host can either be empty, or match the tls.Host[0]
value exactly.

Fixes #9548

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-12 06:40:52 -07:00
Maisem Ali
2d4f808a4c cmd/containerboot: fix time based serveConfig watcher
This broke in a last minute refactor and seems to have never worked.

Fixes #9686

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-12 06:36:40 -07:00
James Tucker
4abd470322 tailcfg: implement text encoding for ProtoPortRange
Updates tailscale/corp#15043
Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 23:59:42 -07:00
James Tucker
96f01a73b1 tailcfg: import ProtoPortRange for local use
Imported type and parsing, with minor modifications.

Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 23:46:39 -07:00
Charlotte Brandhorst-Satzkorn
d62af8e643 words: flappy birds, but real life
These birds have been visually identified as having tails. Science
prevails.

Updates tailscale/corp#9599

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-10-11 22:39:30 -07:00
Maisem Ali
1cb9e33a95 cmd/k8s-operator: update env var in manifest to APISERVER_PROXY
Replace the deprecated var with the one in docs to avoid confusion.
Introduced in 335a5aaf9a.

Updates #8317
Fixes #9764

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 21:55:46 -07:00
James Tucker
c1ef55249a types/ipproto: import and test string parsing for ipproto
IPProto has been being converted to and from string formats in multiple
locations with variations in behavior. TextMarshaller and JSONMarshaller
implementations are now added, along with defined accepted and preferred
formats to centralize the logic into a single cross compatible
implementation.

Updates tailscale/corp#15043
Fixes tailscale/corp#15141

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 18:56:33 -07:00
Maisem Ali
319607625f ipn/ipnlocal: fix log spam from now expected paths
These log paths were actually unexpected until the refactor in
fe95d81b43. This moves the logs
to the callsites where they are actually unexpected.

Fixes #9670

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 14:53:58 -07:00
Maisem Ali
9d96e05267 net/packet: split off checksum munging into different pkg
The current structure meant that we were embedding netstack in
the tailscale CLI and in the GUIs. This removes that by isolating
the checksum munging to a different pkg which is only called from
`net/tstun`.

Fixes #9756

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 14:25:58 -07:00
Brad Fitzpatrick
8b630c91bc wgengine/filter: use slices.Contains in another place
We keep finding these.

Updates #cleanup

Change-Id: Iabc049b0f8da07341011356f0ecd5315c33ff548
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-11 14:16:52 -07:00
James 'zofrex' Sanderson
0a412eba40 words: Na na na na na na na na na na na na na na na na (#9753)
So gnarly!

Updates tailscale/corp#14698

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2023-10-11 22:15:53 +01:00
James Tucker
11348fbe72 util/nocasemaps: import nocasemaps from corp
This is a dependency of other code being imported later.

Updates tailscale/corp#15043

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-11 13:55:00 -07:00
Maisem Ali
fbfee6a8c0 cmd/containerboot: use linuxfw.NetfilterRunner
This migrates containerboot to reuse the NetfilterRunner used
by tailscaled instead of manipulating iptables rule itself.
This has the added advantage of now working with nftables and
we can potentially drop the `iptables` command from the container
image in the future.

Updates #9310

Co-authored-by: Irbe Krumina <irbe@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 12:23:52 -07:00
Sonia Appasamy
7a0de2997e client/web: remove unused context param from NewServer
Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-10-11 15:06:26 -04:00
Maisem Ali
aad3584319 util/linuxfw: move fake runner into pkg
This allows using the fake runner in different packages
that need to manage filter rules.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-11 11:48:43 -07:00
Tom DNetto
fffafc65d6 tsnet: support registering fallback TCP flow handlers
For the app connector use-case, it doesnt make sense to use listeners, because then you would
need to register thousands of listeners (for each proto/service/port combo) to handle ranges.

Instead, we plumb through the TCPHandlerForFlow abstraction, to avoid using the listeners
abstraction that would end up being a bit messy.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates: https://github.com/tailscale/corp/issues/15038
2023-10-11 11:29:29 -07:00
David Anderson
9f05018419 clientupdate/distsign: add new prod root signing key to keychain
Updates tailscale/corp#15179

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-10-11 09:20:17 -07:00
Galen Guyer
04a8b8bb8e net/dns: properly detect newer debian resolvconf
Tailscale attempts to determine if resolvconf or openresolv
is in use by running `resolvconf --version`, under the assumption
this command will error when run with Debian's resolvconf. This
assumption is no longer true and leads to the wrong commands being
run on newer versions of Debian with resolvconf >= 1.90. We can
now check if the returned version string starts with "Debian resolvconf"
if the command is successful.

Fixes #9218

Signed-off-by: Galen Guyer <galen@galenguyer.com>
2023-10-11 08:38:25 -07:00
Paul Scott
4e083e4548 util/cmpver: only consider ascii numerals (#9741)
Fixes #9740

Signed-off-by: Paul Scott <paul@tailscale.com>
2023-10-11 13:42:32 +01:00
Maisem Ali
78a083e144 types/ipproto: drop IPProto from IPProtoVersion
Based on https://github.com/golang/go/wiki/CodeReviewComments#package-names.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-10 23:44:48 -07:00
Maisem Ali
05a1f5bf71 util/linuxfw: move detection logic
Just a refactor to consolidate the firewall detection logic in a single
package so that it can be reused in a later commit by containerboot.

Updates #9310

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-10 20:29:24 -07:00
Maisem Ali
56c0a75ea9 tool/gocross: handle VERSION file not found
Fixes #9734

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-10-10 17:55:33 -07:00
James Tucker
ba6ec42f6d util/linuxfw: add missing input rule to the tailscale tun
Add an explicit accept rule for input to the tun interface, as a mirror
to the explicit rule to accept output from the tun interface.

The rule matches any packet in to our tun interface and accepts it, and
the rule is positioned and prioritized such that it should be evaluated
prior to conventional ufw/iptables/nft rules.

Updates #391
Fixes #7332
Updates #9084

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-10 17:22:47 -07:00
Andrew Lytvynov
677d486830 clientupdate: abort if current version is newer than latest (#9733)
This is only relevant for unstable releases and local builds. When local
version is newer than upstream, abort release.

Also, re-add missing newlines in output that were missed in
https://github.com/tailscale/tailscale/pull/9694.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-10 17:01:44 -07:00
Will Norris
7f08bddfe1 tailcfg: add type for web client auth response
This will be returned from the upcoming control endpoints for doing web
client session authentication.

Updates tailscale/corp#14335

Signed-off-by: Will Norris <will@tailscale.com>
2023-10-10 15:13:50 -07:00
Flakes Updater
00977f6de9 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-10-10 14:43:58 -07:00
License Updater
0ccfcb515c licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-10-10 14:43:50 -07:00
Brad Fitzpatrick
3749a3bbbb go.toolchain.rev: bump for CVE-2023-39325
Updates tailscale/corp#15165

Change-Id: Ib001cfb44eb3e6d735dfece9bd3ae9eea13048c9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-10 11:46:38 -07:00
Brad Fitzpatrick
6b1ed732df go.mod: bump x/net to 0.17 for CVE-2023-39325
https://go.googlesource.com/net/+/b225e7ca6dde1ef5a5ae5ce922861bda011cfabd

Updates tailscale/corp#15165

Change-Id: Ia8b5e16b1acfe1b2400d321034b41370396f70e2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-10 11:25:24 -07:00
Brad Fitzpatrick
70de16bda7 ipn/localapi: make whois take IP or IP:port as documented, fix capmap netstack lookup
The whois handler was documented as taking IP (e.g. 100.101.102.103)
or IP:port (e.g. usermode 127.0.0.1:1234) but that got broken at some point
and we started requiring a port always. Fix that.

Also, found in the process of adding tests: fix the CapMap lookup in
userspace mode (it was always returning the caps of 127.0.0.1 in
userspace mode). Fix and test that too.

Updates #9714

Change-Id: Ie9a59744286522fa91c4b70ebe89a1e94dbded26
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-10 11:05:04 -07:00
Kristoffer Dalby
7f540042d5 ipn/ipnlocal: use syspolicy to determine collection of posture data
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-10 12:04:34 +02:00
Kristoffer Dalby
d0b8bdf8f7 posture: add get serial support for macOS
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 14:46:59 +02:00
Kristoffer Dalby
9eedf86563 posture: add get serial support for Windows/Linux
This commit adds support for getting serial numbers from SMBIOS
on Windows/Linux (and BSD) using go-smbios.

Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 13:50:34 +02:00
Val
249edaa349 wgengine/magicsock: add probed MTU metrics
Record the number of MTU probes sent, the total bytes sent, the number of times
we got a successful return from an MTU probe of a particular size, and the max
MTU recorded.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-10-09 01:57:12 -07:00
Val
893bdd729c disco,net/tstun,wgengine/magicsock: probe peer MTU
Automatically probe the path MTU to a peer when peer MTU is enabled, but do not
use the MTU information for anything yet.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-10-09 01:57:12 -07:00
Kristoffer Dalby
b4e587c3bd tailcfg,ipn: add c2n endpoint for posture identity
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 08:15:38 +02:00
Kristoffer Dalby
9593cd3871 posture: add get serial stub for all platforms
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 08:15:38 +02:00
Kristoffer Dalby
623926a25d cmd/tailscale: add --posture-checking flag to set
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 08:15:38 +02:00
Kristoffer Dalby
886917c42b ipn: add PostureChecks to Prefs
Updates #5902

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-10-09 08:15:38 +02:00
Simon Leonhardt
553f657248 sniproxy allows configuration of hostname
Signed-off-by: Simon Leonhardt <simon@controlzee.com>
2023-10-08 15:53:52 -07:00
Brad Fitzpatrick
6f36f8842c cmd/tailscale, magicsock: add debug command to flip DERP homes
For testing netmap patchification server-side.

Updates #1909

Change-Id: Ib1d784bd97b8d4a31e48374b4567404aae5280cc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-06 20:48:13 -07:00
James Tucker
13767e5108 docs/sysv: add a sysv style init script
The script depends on a sufficiently recent start-stop-daemon as to
provide the `-m` and `--remove-pidfile` flags.

Updates #9502

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-06 19:35:58 -07:00
Brad Fitzpatrick
f991c8a61f tstest: make ResourceCheck panic on parallel tests
To find potential flakes earlier.

Updates #deflake-effort

Change-Id: I52add6111d660821c3a23d4b1dd032821344bc48
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-06 19:12:34 -07:00
James Tucker
498f7ec663 syncs: add Map.LoadOrInit for lazily initialized values
I was reviewing some code that was performing this by hand, and wanted
to suggest using syncs.Map, however as the code in question was
allocating a non-trivial structure this would be necessary to meet the
target.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-10-06 17:06:11 -07:00
Joe Tsai
e4cb83b18b taildrop: document and cleanup the package (#9699)
Changes made:
* Unexport declarations specific to internal taildrop functionality.
* Document all exported functionality.
* Move TestRedactErr to the taildrop package.
* Rename and invert Handler.DirectFileDoFinalRename as AvoidFinalRename.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-10-06 15:41:14 -07:00
Andrew Lytvynov
e6aa7b815d clientupdate,cmd/tailscale/cli: use cli.Stdout/Stderr (#9694)
In case cli.Stdout/Stderr get overriden, all CLI output should use them
instead of os.Stdout/Stderr. Update the `update` command to follow this
pattern.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-10-06 12:00:15 -07:00
Brad Fitzpatrick
b7988b3825 api.md: remove clientConnectivity.derp field
We don't actually send this. It's always been empty.

Updates tailscale/corp#13400

Change-Id: I99b3d7a355fca17d2159bf81ede5be4ddd4b9dc9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-06 09:29:42 -07:00
Rhea Ghosh
557ddced6c {ipn/ipnlocal, taildrop}: move put logic to taildrop (#9680)
Cleaning up taildrop logic for sending files.

Updates tailscale/corp#14772

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
Co-authored-by: Joe Tsai <joetsai@digital-static.net>
2023-10-06 09:47:03 -05:00
127 changed files with 5538 additions and 1754 deletions

View File

@@ -39,6 +39,16 @@ concurrency:
cancel-in-progress: true
jobs:
race-root-integration:
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v4
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: integration tests as root
run: PATH=$PWD/tool:$PATH /tmp/testwrapper --sudo ./tstest/integration/ -race
test:
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
@@ -70,6 +80,7 @@ jobs:
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
- name: build all
if: matrix.buildflags == '' # skip on race builder
run: ./tool/go build ${{matrix.buildflags}} ./...
env:
GOARCH: ${{ matrix.goarch }}
@@ -162,7 +173,17 @@ jobs:
HOME: "/tmp"
TMPDIR: "/tmp"
XDB_CACHE_HOME: "/var/lib/ghrunner/cache"
race-build:
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v4
- name: build all
run: ./tool/go install -race ./cmd/...
- name: build tests
run: ./tool/go test -race -exec=true ./...
cross: # cross-compile checks, build only.
strategy:
fail-fast: false # don't abort the entire matrix if one element fails

4
api.md
View File

@@ -209,10 +209,6 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
"192.68.0.21:59128"
],
// derp (string) is the IP:port of the DERP server currently being used.
// Learn about DERP servers at https://tailscale.com/kb/1232/.
"derp":"",
// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
// vary based on the destination IP.
"mappingVariesByDestIP":false,

59
appctype/appconnector.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package appcfg contains an experimental configuration structure for
// "tailscale.com/app-connector" capmap extensions.
package appctype
import (
"net/netip"
"tailscale.com/tailcfg"
)
// ConfigID is an opaque identifier for a configuration.
type ConfigID string
// AppConnectorConfig is the configuration structure for an application
// connection proxy service.
type AppConnectorConfig struct {
// DNAT is a map of destination NAT configurations.
DNAT map[ConfigID]DNATConfig `json:",omitempty"`
// SNIProxy is a map of SNI proxy configurations.
SNIProxy map[ConfigID]SNIProxyConfig `json:",omitempty"`
// AdvertiseRoutes indicates that the node should advertise routes for each
// of the addresses in service configuration address lists. If false, the
// routes have already been advertised.
AdvertiseRoutes bool `json:",omitempty"`
}
// DNATConfig is the configuration structure for a destination NAT service, also
// known as a "port forward" or "port proxy".
type DNATConfig struct {
// Addrs is a list of addresses to listen on.
Addrs []netip.Addr `json:",omitempty"`
// To is a list of destination addresses to forward traffic to. It should
// only contain one domain, or a list of IP addresses.
To []string `json:",omitempty"`
// IP is a list of IP specifications to forward. If omitted, all protocols are
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
IP []tailcfg.ProtoPortRange `json:",omitempty"`
}
// SNIPRoxyConfig is the configuration structure for an SNI proxy service,
// forwarding TLS connections based on the hostname field in SNI.
type SNIProxyConfig struct {
// Addrs is a list of addresses to listen on.
Addrs []netip.Addr `json:",omitempty"`
// IP is a list of IP specifications to forward. If omitted, all protocols are
// forwarded. IP specifications are of the form "tcp/80", "udp/53", etc.
IP []tailcfg.ProtoPortRange `json:",omitempty"`
// AllowedDomains is a list of domains that are allowed to be proxied. If
// the domain starts with a `.` that means any subdomain of the suffix.
AllowedDomains []string `json:",omitempty"`
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package appctype
import (
"encoding/json"
"net/netip"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/tailcfg"
"tailscale.com/util/must"
)
var golden = `{
"dnat": {
"opaqueid1": {
"addrs": ["100.64.0.1", "fd7a:115c:a1e0::1"],
"to": ["example.org"],
"ip": ["*"]
}
},
"sniProxy": {
"opaqueid2": {
"addrs": ["::"],
"ip": ["tcp:443"],
"allowedDomains": ["*"]
}
},
"advertiseRoutes": true
}`
func TestGolden(t *testing.T) {
wantDNAT := map[ConfigID]DNATConfig{"opaqueid1": {
Addrs: []netip.Addr{netip.MustParseAddr("100.64.0.1"), netip.MustParseAddr("fd7a:115c:a1e0::1")},
To: []string{"example.org"},
IP: []tailcfg.ProtoPortRange{{Proto: 0, Ports: tailcfg.PortRange{First: 0, Last: 65535}}},
}}
wantSNI := map[ConfigID]SNIProxyConfig{"opaqueid2": {
Addrs: []netip.Addr{netip.MustParseAddr("::")},
IP: []tailcfg.ProtoPortRange{{Proto: 6, Ports: tailcfg.PortRange{First: 443, Last: 443}}},
AllowedDomains: []string{"*"},
}}
var config AppConnectorConfig
if err := json.NewDecoder(strings.NewReader(golden)).Decode(&config); err != nil {
t.Fatalf("failed to decode golden config: %v", err)
}
if !config.AdvertiseRoutes {
t.Fatalf("expected AdvertiseRoutes to be true, got false")
}
assertEqual(t, "DNAT", config.DNAT, wantDNAT)
assertEqual(t, "SNI", config.SNIProxy, wantSNI)
}
func TestRoundTrip(t *testing.T) {
var config AppConnectorConfig
must.Do(json.NewDecoder(strings.NewReader(golden)).Decode(&config))
b := must.Get(json.Marshal(config))
var config2 AppConnectorConfig
must.Do(json.Unmarshal(b, &config2))
assertEqual(t, "DNAT", config.DNAT, config2.DNAT)
}
func assertEqual(t *testing.T, name string, a, b any) {
var addrComparer = cmp.Comparer(func(a, b netip.Addr) bool {
return a.Compare(b) == 0
})
t.Helper()
if diff := cmp.Diff(a, b, addrComparer); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}

View File

@@ -40,3 +40,12 @@ type SetPushDeviceTokenRequest struct {
// PushDeviceToken is the iOS/macOS APNs device token (and any future Android equivalent).
PushDeviceToken string
}
// ReloadConfigResponse is the response to a LocalAPI reload-config request.
//
// There are three possible outcomes: (false, "") if no config mode in use,
// (true, "") on success, or (false, "error message") on failure.
type ReloadConfigResponse struct {
Reloaded bool // whether the config was reloaded
Err string // any error message
}

View File

@@ -1244,6 +1244,25 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
return current, all, err
}
// ReloadConfig reloads the config file, if possible.
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
if err != nil {
return
}
res, err := decodeJSON[apitype.ReloadConfigResponse](body)
if err != nil {
return
}
if err != nil {
return false, err
}
if res.Err != "" {
return false, errors.New(res.Err)
}
return res.Reloaded, nil
}
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
// profile is not assigned an ID until it is persisted after a successful login.
// In order to login to the new profile, the user must call LoginInteractive.

View File

@@ -66,6 +66,11 @@ const (
sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
)
var (
exitNodeRouteV4 = netip.MustParsePrefix("0.0.0.0/0")
exitNodeRouteV6 = netip.MustParsePrefix("::/0")
)
// browserSession holds data about a user's browser session
// on the full management web client.
type browserSession struct {
@@ -125,8 +130,7 @@ type ServerOpts struct {
}
// NewServer constructs a new Tailscale web client server.
// The provided context should live for the duration of the Server's lifetime.
func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func()) {
func NewServer(opts ServerOpts) (s *Server, cleanup func()) {
if opts.LocalClient == nil {
opts.LocalClient = &tailscale.LocalClient{}
}
@@ -190,7 +194,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
return
}
if !s.devMode {
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
s.lc.IncrementCounter(r.Context(), "web_client_page_load", 1)
}
s.assetsHandler.ServeHTTP(w, r)
}
@@ -428,8 +432,6 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
IPNVersion: versionShort,
DebugMode: s.tsDebugMode,
}
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
for _, r := range prefs.AdvertiseRoutes {
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
data.AdvertiseExitNode = true
@@ -474,6 +476,22 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
return
}
prefs, err := s.lc.GetPrefs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
isCurrentlyExitNode := slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV4) || slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV6)
if postData.AdvertiseExitNode != isCurrentlyExitNode {
if postData.AdvertiseExitNode {
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_enable", 1)
} else {
s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_disable", 1)
}
}
routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)

View File

@@ -30,6 +30,7 @@ import (
"github.com/google/uuid"
"tailscale.com/clientupdate/distsign"
"tailscale.com/types/logger"
"tailscale.com/util/cmpver"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -77,6 +78,10 @@ type Arguments struct {
AppStore bool
// Logf is a logger for update progress messages.
Logf logger.Logf
// Stdout and Stderr should be used for output instead of os.Stdout and
// os.Stderr.
Stdout io.Writer
Stderr io.Writer
// Confirm is called when a new version is available and should return true
// if this new version should be installed. When Confirm returns false, the
// update is aborted.
@@ -108,6 +113,12 @@ func NewUpdater(args Arguments) (*Updater, error) {
up := Updater{
Arguments: args,
}
if up.Stdout == nil {
up.Stdout = os.Stdout
}
if up.Stderr == nil {
up.Stderr = os.Stderr
}
up.Update = up.getUpdateFunction()
if up.Update == nil {
return nil, errors.ErrUnsupported
@@ -174,7 +185,9 @@ func (up *Updater) getUpdateFunction() updateFunction {
case !up.Arguments.AppStore && !version.IsSandboxedMacOS():
return nil
case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
return up.updateMacSys
// TODO(noncombatant): return up.updateMacSys when we figure out why
// Sparkle update doesn't work when running "tailscale update".
return nil
default:
return up.updateMacAppStore
}
@@ -201,9 +214,13 @@ func Update(args Arguments) error {
}
func (up *Updater) confirm(ver string) bool {
if version.Short() == ver {
switch cmpver.Compare(version.Short(), ver) {
case 0:
up.Logf("already running %v; no update needed", ver)
return false
case 1:
up.Logf("installed version %v is newer than the latest available version %v; no update needed", version.Short(), ver)
return false
}
if up.Confirm != nil {
return up.Confirm(ver)
@@ -256,9 +273,9 @@ func (up *Updater) updateSynology() error {
// connected over tailscale ssh and this parent process dies. Otherwise, if
// you abort synopkg install mid-way, tailscaled is not restarted.
cmd := exec.Command("nohup", "synopkg", "install", spkPath)
// Don't attach cmd.Stdout to os.Stdout because nohup will redirect that
// into nohup.out file. synopkg doesn't have any progress output anyway, it
// just spits out a JSON result when done.
// Don't attach cmd.Stdout to Stdout because nohup will redirect that into
// nohup.out file. synopkg doesn't have any progress output anyway, it just
// spits out a JSON result when done.
out, err := cmd.CombinedOutput()
if err != nil {
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
@@ -369,15 +386,15 @@ func (up *Updater) updateDebLike() error {
// we're not updating them:
"-o", "APT::Get::List-Cleanup=0",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -491,8 +508,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -577,8 +594,8 @@ func (up *Updater) updateAlpineLike() (err error) {
}
cmd := exec.Command("apk", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using apk: %w", err)
}
@@ -634,8 +651,8 @@ func (up *Updater) updateMacAppStore() error {
}
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
@@ -726,8 +743,8 @@ func (up *Updater) updateWindows() error {
cmd := exec.Command(selfCopy, "update")
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stderr
cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
return err
@@ -743,8 +760,8 @@ func (up *Updater) installMSI(msi string) error {
for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
cmd.Dir = filepath.Dir(msi)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err == nil {
@@ -757,8 +774,8 @@ func (up *Updater) installMSI(msi string) error {
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
up.Logf("msiexec uninstall: %v", err)
@@ -846,8 +863,8 @@ func (up *Updater) updateFreeBSD() (err error) {
}
cmd := exec.Command("pkg", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pkg: %w", err)
}

View File

@@ -1,3 +1,3 @@
-----BEGIN ROOT PUBLIC KEY-----
Muw5GkO5mASsJ7k6kS+svfuanr6XcW9I7fPGtyqOTeI=
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
-----END ROOT PUBLIC KEY-----

View File

@@ -19,8 +19,7 @@
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
// destination.
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
// - TS_EXTRA_ARGS: extra arguments to 'tailscale login', these are not
// reset on restart.
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
// - TS_USERSPACE: run with userspace networking (the default)
// instead of kernel networking.
// - TS_STATE_DIR: the directory in which to store tailscaled
@@ -36,15 +35,9 @@
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
// be created.
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
// logged in. If false, forcibly log in every time the container starts.
// The default until 1.50.0 was false, but that was misleading: until
// 1.50, containerboot used `tailscale up` which would ignore an authkey
// argument if there was already a node key. Effectively, this behaved
// as though TS_AUTH_ONCE were always true.
// In 1.50.0 the change was made to use `tailscale login` instead of `up`,
// and login will reauthenticate every time it is given an authkey.
// In 1.50.1 we set the TS_AUTH_ONCE to true, to match the previously
// observed behavior.
// logged in. If false (the default, for backwards
// compatibility), forcibly log in every time the
// container starts.
// - TS_SERVE_CONFIG: if specified, is the file path where the ipn.ServeConfig is located.
// It will be applied once tailscaled is up and running. If the file contains
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
@@ -84,10 +77,19 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/types/logger"
"tailscale.com/types/ptr"
"tailscale.com/util/deephash"
"tailscale.com/util/linuxfw"
)
func newNetfilterRunner(logf logger.Logf) (linuxfw.NetfilterRunner, error) {
if defaultBool("TS_TEST_FAKE_NETFILTER", false) {
return linuxfw.NewFakeIPTablesRunner(), nil
}
return linuxfw.New(logf)
}
func main() {
log.SetPrefix("boot: ")
tailscale.I_Acknowledge_This_API_Is_Unstable = true
@@ -109,7 +111,7 @@ func main() {
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
AuthOnce: defaultBool("TS_AUTH_ONCE", true),
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
}
@@ -203,7 +205,7 @@ func main() {
}
didLogin = true
w.Close()
if err := tailscaleLogin(bootCtx, cfg); err != nil {
if err := tailscaleUp(bootCtx, cfg); err != nil {
return fmt.Errorf("failed to auth tailscale: %v", err)
}
w, err = client.WatchIPNBus(bootCtx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
@@ -253,10 +255,12 @@ authLoop:
ctx, cancel := context.WithCancel(context.Background()) // no deadline now that we're in steady state
defer cancel()
// Now that we are authenticated, we can set/reset any of the
// settings that we need to.
if err := tailscaleSet(ctx, cfg); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
if cfg.AuthOnce {
// Now that we are authenticated, we can set/reset any of the
// settings that we need to.
if err := tailscaleSet(ctx, cfg); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
}
if cfg.ServeConfigPath != "" {
@@ -295,6 +299,13 @@ authLoop:
if cfg.ServeConfigPath != "" {
go watchServeConfigChanges(ctx, cfg.ServeConfigPath, certDomainChanged, certDomain, client)
}
var nfr linuxfw.NetfilterRunner
if wantProxy {
nfr, err = newNetfilterRunner(log.Printf)
if err != nil {
log.Fatalf("error creating new netfilter runner: %v", err)
}
}
for {
n, err := w.Next()
if err != nil {
@@ -315,7 +326,7 @@ authLoop:
ipsHaveChanged := newCurrentIPs != currentIPs
if cfg.ProxyTo != "" && len(addrs) > 0 && ipsHaveChanged {
log.Printf("Installing proxy rules")
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs); err != nil {
if err := installIngressForwardingRule(ctx, cfg.ProxyTo, addrs, nfr); err != nil {
log.Fatalf("installing ingress proxy rules: %v", err)
}
}
@@ -330,7 +341,7 @@ authLoop:
}
}
if cfg.TailnetTargetIP != "" && ipsHaveChanged && len(addrs) > 0 {
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs); err != nil {
if err := installEgressForwardingRule(ctx, cfg.TailnetTargetIP, addrs, nfr); err != nil {
log.Fatalf("installing egress proxy rules: %v", err)
}
}
@@ -385,19 +396,20 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
panic("cd must not be nil")
}
var tickChan <-chan time.Time
w, err := fsnotify.NewWatcher()
if err != nil {
var eventChan <-chan fsnotify.Event
if w, err := fsnotify.NewWatcher(); err != nil {
log.Printf("failed to create fsnotify watcher, timer-only mode: %v", err)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
tickChan = ticker.C
} else {
defer w.Close()
if err := w.Add(filepath.Dir(path)); err != nil {
log.Fatalf("failed to add fsnotify watch: %v", err)
}
eventChan = w.Events
}
if err := w.Add(filepath.Dir(path)); err != nil {
log.Fatalf("failed to add fsnotify watch: %v", err)
}
var certDomain string
var prevServeConfig *ipn.ServeConfig
for {
@@ -407,7 +419,7 @@ func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan
case <-cdChanged:
certDomain = *certDomainAtomic.Load()
case <-tickChan:
case <-w.Events:
case <-eventChan:
// We can't do any reasonable filtering on the event because of how
// k8s handles these mounts. So just re-read the file and apply it
// if it's changed.
@@ -528,29 +540,40 @@ func tailscaledArgs(cfg *settings) []string {
return args
}
// tailscaleLogin uses cfg to run 'tailscale login' everytime containerboot
// starts, or if TS_AUTH_ONCE is set, only the first time containerboot starts.
func tailscaleLogin(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "login"}
// tailscaleUp uses cfg to run 'tailscale up' everytime containerboot starts, or
// if TS_AUTH_ONCE is set, only the first time containerboot starts.
func tailscaleUp(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "up"}
if cfg.AcceptDNS {
args = append(args, "--accept-dns=true")
} else {
args = append(args, "--accept-dns=false")
}
if cfg.AuthKey != "" {
args = append(args, "--authkey="+cfg.AuthKey)
}
if cfg.Routes != "" {
args = append(args, "--advertise-routes="+cfg.Routes)
}
if cfg.Hostname != "" {
args = append(args, "--hostname="+cfg.Hostname)
}
if cfg.ExtraArgs != "" {
args = append(args, strings.Fields(cfg.ExtraArgs)...)
}
log.Printf("Running 'tailscale login'")
log.Printf("Running 'tailscale up'")
cmd := exec.CommandContext(ctx, "tailscale", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("tailscale login failed: %v", err)
return fmt.Errorf("tailscale up failed: %v", err)
}
return nil
}
// tailscaleSet uses cfg to run 'tailscale set' to set any known configuration
// options that are passed in via environment variables. This is run after the
// node is in Running state.
// node is in Running state and only if TS_AUTH_ONCE is set.
func tailscaleSet(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "set"}
if cfg.AcceptDNS {
@@ -662,16 +685,12 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, routes string
return nil
}
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
dst, err := netip.ParseAddr(dstStr)
if err != nil {
return err
}
argv0 := "iptables"
if dst.Is6() {
argv0 = "ip6tables"
}
var local string
var local netip.Addr
for _, pfx := range tsIPs {
if !pfx.IsSingleIP() {
continue
@@ -679,52 +698,30 @@ func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []net
if pfx.Addr().Is4() != dst.Is4() {
continue
}
local = pfx.Addr().String()
local = pfx.Addr()
break
}
if local == "" {
if !local.IsValid() {
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
}
// Technically, if the control server ever changes the IPs assigned to this
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
// for now we'll live with it.
// Set up a rule that ensures that all packets
// except for those received on tailscale0 interface is forwarded to
// destination address
cmdDNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "!", "-i", "tailscale0", "-j", "DNAT", "--to-destination", dstStr)
cmdDNAT.Stdout = os.Stdout
cmdDNAT.Stderr = os.Stderr
if err := cmdDNAT.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
if err := nfr.DNATNonTailscaleTraffic("tailscale0", dst); err != nil {
return fmt.Errorf("installing egress proxy rules: %w", err)
}
// Set up a rule that ensures that all packets sent to the destination
// address will have the proxy's IP set as source IP
cmdSNAT := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "POSTROUTING", "1", "--destination", dstStr, "-j", "SNAT", "--to-source", local)
cmdSNAT.Stdout = os.Stdout
cmdSNAT.Stderr = os.Stderr
if err := cmdSNAT.Run(); err != nil {
return fmt.Errorf("setting up SNAT via iptables failed: %w", err)
if err := nfr.AddSNATRuleForDst(local, dst); err != nil {
return fmt.Errorf("installing egress proxy rules: %w", err)
}
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
cmdClamp.Stdout = os.Stdout
cmdClamp.Stderr = os.Stderr
if err := cmdClamp.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
return fmt.Errorf("installing egress proxy rules: %w", err)
}
return nil
}
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
dst, err := netip.ParseAddr(dstStr)
if err != nil {
return err
}
argv0 := "iptables"
if dst.Is6() {
argv0 = "ip6tables"
}
var local string
var local netip.Addr
for _, pfx := range tsIPs {
if !pfx.IsSingleIP() {
continue
@@ -732,26 +729,17 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
if pfx.Addr().Is4() != dst.Is4() {
continue
}
local = pfx.Addr().String()
local = pfx.Addr()
break
}
if local == "" {
if !local.IsValid() {
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
}
// Technically, if the control server ever changes the IPs assigned to this
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
// for now we'll live with it.
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
if err := nfr.AddDNATRule(local, dst); err != nil {
return fmt.Errorf("installing ingress proxy rules: %w", err)
}
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
cmdClamp.Stdout = os.Stdout
cmdClamp.Stderr = os.Stderr
if err := cmdClamp.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
if err := nfr.ClampMSSToPMTU("tailscale0", dst); err != nil {
return fmt.Errorf("installing ingress proxy rules: %w", err)
}
return nil
}

View File

@@ -129,22 +129,16 @@ func TestContainerBoot(t *testing.T) {
{
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
Name: "no_args",
Env: map[string]string{
"TS_AUTH_ONCE": "false",
},
Env: nil,
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
@@ -152,21 +146,17 @@ func TestContainerBoot(t *testing.T) {
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_AUTH_ONCE": "false",
"TS_AUTHKEY": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
@@ -174,21 +164,17 @@ func TestContainerBoot(t *testing.T) {
// Userspace mode, ephemeral storage, authkey provided on every run.
Name: "authkey-old-flag",
Env: map[string]string{
"TS_AUTH_KEY": "tskey-key",
"TS_AUTH_ONCE": "false",
"TS_AUTH_KEY": "tskey-key",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
@@ -197,35 +183,30 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
{
Name: "routes",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
"TS_AUTH_ONCE": "false",
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
{
@@ -234,9 +215,6 @@ func TestContainerBoot(t *testing.T) {
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
},
},
@@ -246,13 +224,12 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
"TS_USERSPACE": "false",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
{
@@ -261,9 +238,6 @@ func TestContainerBoot(t *testing.T) {
"proc/sys/net/ipv4/ip_forward": "1",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=1.2.3.0/24,10.20.30.0/24",
},
},
},
},
@@ -273,13 +247,12 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1::/64",
"TS_USERSPACE": "false",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1::/64",
},
},
{
@@ -288,9 +261,6 @@ func TestContainerBoot(t *testing.T) {
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "1",
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1::/64",
},
},
},
},
@@ -300,13 +270,12 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "::/64,1.2.3.0/24",
"TS_USERSPACE": "false",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=::/64,1.2.3.0/24",
},
},
{
@@ -315,9 +284,6 @@ func TestContainerBoot(t *testing.T) {
"proc/sys/net/ipv4/ip_forward": "1",
"proc/sys/net/ipv6/conf/all/forwarding": "1",
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --advertise-routes=::/64,1.2.3.0/24",
},
},
},
},
@@ -327,22 +293,16 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_DEST_IP": "1.2.3.4",
"TS_USERSPACE": "false",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
},
},
},
},
@@ -352,23 +312,16 @@ func TestContainerBoot(t *testing.T) {
"TS_AUTHKEY": "tskey-key",
"TS_TAILNET_TARGET_IP": "100.99.99.99",
"TS_USERSPACE": "false",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
"/usr/bin/iptables -t nat -I PREROUTING 1 ! -i tailscale0 -j DNAT --to-destination 100.99.99.99",
"/usr/bin/iptables -t nat -I POSTROUTING 1 --destination 100.99.99.99 -j SNAT --to-source 100.64.0.1",
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
},
},
},
},
@@ -389,7 +342,7 @@ func TestContainerBoot(t *testing.T) {
State: ptr.To(ipn.NeedsLogin),
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
},
{
@@ -405,7 +358,6 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -414,7 +366,7 @@ func TestContainerBoot(t *testing.T) {
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -422,9 +374,6 @@ func TestContainerBoot(t *testing.T) {
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net",
@@ -443,22 +392,18 @@ func TestContainerBoot(t *testing.T) {
"TS_KUBE_SECRET": "",
"TS_STATE_DIR": filepath.Join(d, "tmp"),
"TS_AUTHKEY": "tskey-key",
"TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
Notify: runningNotify,
WantKubeSecret: map[string]string{},
},
},
@@ -469,7 +414,6 @@ func TestContainerBoot(t *testing.T) {
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTHKEY": "tskey-key",
"TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{},
KubeDenyPatch: true,
@@ -477,15 +421,12 @@ func TestContainerBoot(t *testing.T) {
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
Notify: runningNotify,
WantKubeSecret: map[string]string{},
},
},
@@ -515,7 +456,7 @@ func TestContainerBoot(t *testing.T) {
State: ptr.To(ipn.NeedsLogin),
},
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -539,7 +480,6 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"KUBERNETES_SERVICE_HOST": kube.Host,
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
"TS_AUTH_ONCE": "false",
},
KubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -548,7 +488,7 @@ func TestContainerBoot(t *testing.T) {
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=kube:tailscale --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --authkey=tskey-key",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
@@ -556,9 +496,6 @@ func TestContainerBoot(t *testing.T) {
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
WantKubeSecret: map[string]string{
"authkey": "tskey-key",
"device_fqdn": "test-node.test.ts.net",
@@ -591,20 +528,16 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_SOCKS5_SERVER": "localhost:1080",
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --socks5-server=localhost:1080 --outbound-http-proxy-listen=localhost:8080",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
@@ -612,20 +545,16 @@ func TestContainerBoot(t *testing.T) {
Name: "dns",
Env: map[string]string{
"TS_ACCEPT_DNS": "true",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=true",
},
},
{
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=true",
},
},
},
},
@@ -634,41 +563,31 @@ func TestContainerBoot(t *testing.T) {
Env: map[string]string{
"TS_EXTRA_ARGS": "--widget=rotated",
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
"TS_AUTH_ONCE": "false",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --experiments=widgets",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login --widget=rotated",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --widget=rotated",
},
},
{
}, {
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
},
},
},
},
{
Name: "hostname",
Env: map[string]string{
"TS_HOSTNAME": "my-server",
"TS_AUTH_ONCE": "false",
"TS_HOSTNAME": "my-server",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock login",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --hostname=my-server",
},
},
{
}, {
Notify: runningNotify,
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false --hostname=my-server",
},
},
},
},
@@ -694,6 +613,7 @@ func TestContainerBoot(t *testing.T) {
fmt.Sprintf("TS_TEST_SOCKET=%s", lapi.Path),
fmt.Sprintf("TS_SOCKET=%s", runningSockPath),
fmt.Sprintf("TS_TEST_ONLY_ROOT=%s", d),
fmt.Sprint("TS_TEST_FAKE_NETFILTER=true"),
}
for k, v := range test.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))

View File

@@ -17,7 +17,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@@ -79,22 +78,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@@ -164,10 +147,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health+
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/vizerror from tailscale.com/tsweb
tailscale.com/util/vizerror from tailscale.com/tsweb+
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+

View File

@@ -12,6 +12,7 @@ import (
"testing"
"tailscale.com/net/stun"
"tailscale.com/tstest/deptest"
)
func TestProdAutocertHostPolicy(t *testing.T) {
@@ -128,3 +129,14 @@ func TestNoContent(t *testing.T) {
})
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
},
}.Check(t)
}

View File

@@ -192,8 +192,15 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
}
}
addIngressBackend(ing.Spec.DefaultBackend, "/")
var tlsHost string // hostname or FQDN or empty
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
tlsHost = ing.Spec.TLS[0].Hosts[0]
}
for _, rule := range ing.Spec.Rules {
if rule.Host != "" {
// Host is optional, but if it's present it must match the TLS host
// otherwise we ignore the rule.
if rule.Host != "" && rule.Host != tlsHost {
a.recorder.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "rule with host %q ignored, unsupported", rule.Host)
continue
}
@@ -208,8 +215,8 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
tags = strings.Split(tstr, ",")
}
hostname := ing.Namespace + "-" + ing.Name + "-ingress"
if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
hostname, _, _ = strings.Cut(ing.Spec.TLS[0].Hosts[0], ".")
if tlsHost != "" {
hostname, _, _ = strings.Cut(tlsHost, ".")
}
sts := &tailscaleSTSConfig{

View File

@@ -151,7 +151,7 @@ spec:
value: tailscale/tailscale:unstable
- name: PROXY_TAGS
value: tag:k8s
- name: AUTH_PROXY
- name: APISERVER_PROXY
value: "false"
volumeMounts:
- name: oauth

View File

@@ -75,6 +75,7 @@ func main() {
wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
hostname = fs.String("hostname", "", "Hostname to register the service under")
)
err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
@@ -89,6 +90,7 @@ func main() {
var s server
s.ts.Port = uint16(*wgPort)
s.ts.Hostname = *hostname
defer s.ts.Close()
lc, err := s.ts.LocalClient()

View File

@@ -139,11 +139,22 @@ var debugCmd = &ffcli.Command{
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
},
{
Name: "pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "switch to some other random DERP home region for a short time",
},
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "force a full no-op netmap update (for load testing)",
},
{
// TODO(bradfitz,maisem): eventually promote this out of debug
Name: "reload-config",
Exec: reloadConfig,
ShortHelp: "reload config",
},
{
Name: "control-knobs",
Exec: debugControlKnobs,
@@ -446,6 +457,20 @@ func localAPIAction(action string) func(context.Context, []string) error {
}
}
func reloadConfig(ctx context.Context, args []string) error {
ok, err := localClient.ReloadConfig(ctx)
if err != nil {
return err
}
if ok {
printf("config reloaded\n")
return nil
}
printf("config mode not in use\n")
os.Exit(1)
panic("unreachable")
}
func runEnv(ctx context.Context, args []string) error {
for _, e := range os.Environ() {
outln(e)

View File

@@ -49,6 +49,7 @@ type setArgsT struct {
forceDaemon bool
updateCheck bool
updateApply bool
postureChecking bool
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -66,6 +67,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -108,6 +111,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
Check: setArgs.updateCheck,
Apply: setArgs.updateApply,
},
PostureChecking: setArgs.postureChecking,
},
}

View File

@@ -114,6 +114,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
if safesocket.GOOSUsesPeerCreds(goos) {
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -725,6 +726,7 @@ func init() {
addPrefFlagMapping("nickname", "ProfileName")
addPrefFlagMapping("update-check", "AutoUpdate")
addPrefFlagMapping("auto-update", "AutoUpdate")
addPrefFlagMapping("posture-checking", "PostureChecking")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {

View File

@@ -63,7 +63,9 @@ func runUpdate(ctx context.Context, args []string) error {
err := clientupdate.Update(clientupdate.Arguments{
Version: ver,
AppStore: updateArgs.appStore,
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
Logf: func(f string, a ...any) { printf(f+"\n", a...) },
Stdout: Stdout,
Stderr: Stderr,
Confirm: confirmUpdate,
})
if errors.Is(err, errors.ErrUnsupported) {

View File

@@ -80,7 +80,7 @@ func runWeb(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
webServer, cleanup := web.NewServer(ctx, web.ServerOpts{
webServer, cleanup := web.NewServer(web.ServerOpts{
DevMode: webArgs.dev,
CGIMode: webArgs.cgi,
PathPrefix: webArgs.prefix,

View File

@@ -17,7 +17,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/fxamacker/cbor/v2 from tailscale.com/tka
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@@ -65,22 +64,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
go4.org/netipx from tailscale.com/wgengine/filter+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
gvisor.dev/gvisor/pkg/tcpip/header from tailscale.com/net/packet
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
@@ -158,7 +141,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/views from tailscale.com/tailcfg+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
@@ -169,11 +152,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/mak from tailscale.com/net/netcheck+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate
tailscale.com/version from tailscale.com/cmd/tailscale/cli+

View File

@@ -0,0 +1,21 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
},
}.Check(t)
}

View File

@@ -86,6 +86,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
@@ -147,6 +148,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tailscale/hujson from tailscale.com/ipn/conffile
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
@@ -238,6 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/control/controlclient+
tailscale.com/ipn from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
@@ -275,6 +278,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
tailscale.com/net/ping from tailscale.com/net/netcheck+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
@@ -292,13 +296,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/control/controlclient+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
@@ -329,7 +334,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
@@ -337,12 +342,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy+
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
@@ -354,11 +361,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
W tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
@@ -386,7 +394,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box

View File

@@ -32,6 +32,7 @@ import (
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnserver"
"tailscale.com/ipn/store"
@@ -127,6 +128,7 @@ var args struct {
tunname string
cleanup bool
confFile string
debug string
port uint16
statepath string
@@ -172,6 +174,7 @@ func main() {
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
flag.StringVar(&args.confFile, "config", "", "path to config file")
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
beCLI()
@@ -339,6 +342,17 @@ func run() error {
sys := new(tsd.System)
// Parse config, if specified, to fail early if it's invalid.
var conf *conffile.Config
if args.confFile != "" {
var err error
conf, err = conffile.Load(args.confFile)
if err != nil {
return fmt.Errorf("error reading config file: %w", err)
}
sys.InitialConfig = conf
}
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
@@ -540,6 +554,10 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
}
sys.Set(store)
if w, ok := sys.Tun.GetOK(); ok {
w.Start()
}
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
if err != nil {
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)

View File

@@ -73,11 +73,15 @@ var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) error {
defer close(ch)
args := []string{"test", "-json", pt.Pattern}
args := []string{"test", "--json"}
if *flagSudo {
args = append(args, "--exec", "sudo -E")
}
args = append(args, pt.Pattern)
args = append(args, otherArgs...)
if len(pt.Tests) > 0 {
runArg := strings.Join(pt.Tests, "|")
args = append(args, "-run", runArg)
args = append(args, "--run", runArg)
}
if debug {
fmt.Println("running", strings.Join(args, " "))
@@ -177,6 +181,11 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []st
return nil
}
var (
flagVerbose = flag.Bool("v", false, "verbose")
flagSudo = flag.Bool("sudo", false, "run tests with -exec=sudo")
)
func main() {
ctx := context.Background()
@@ -187,7 +196,6 @@ func main() {
// We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's
// output.
v := flag.Bool("v", false, "verbose")
flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
@@ -285,7 +293,7 @@ func main() {
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt)
continue
}
if *v || tr.outcome == "fail" {
if *flagVerbose || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs)
}
if tr.outcome != "fail" {

View File

@@ -55,6 +55,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/syspolicy"
"tailscale.com/util/systemd"
)
@@ -566,6 +567,11 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
err = errors.New("hostinfo: BackendLogID missing")
return regen, opt.URL, nil, err
}
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
if err != nil {
c.logf("unable to provide Tailnet field in register request. err: %v", err)
}
now := c.clock.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
@@ -577,6 +583,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
Timestamp: &now,
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
NodeKeySignature: nodeKeySignature,
Tailnet: tailnet,
}
if opt.Logout {
request.Expiry = time.Unix(123, 0) // far in the past

View File

@@ -261,7 +261,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
func MessageSummary(m Message) string {
switch m := m.(type) {
case *Ping:
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case *CallMeMaybe:

63
docs/sysv/tailscale.init Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/sh
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
### BEGIN INIT INFO
# Provides: tailscaled
# Required-Start:
# Required-Stop:
# Default-Start:
# Default-Stop:
# Short-Description: Tailscale Mesh Wireguard VPN
### END INIT INFO
set -e
# /etc/init.d/tailscale: start and stop the Tailscale VPN service
test -x /usr/sbin/tailscaled || exit 0
umask 022
. /lib/lsb/init-functions
# Are we running from init?
run_by_init() {
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
}
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
case "$1" in
start)
log_daemon_msg "Starting Tailscale VPN" "tailscaled" || true
if start-stop-daemon --start --oknodo --name tailscaled -m --pidfile /run/tailscaled.pid --background \
--exec /usr/sbin/tailscaled -- \
--state=/var/lib/tailscale/tailscaled.state \
--socket=/run/tailscale/tailscaled.sock \
--port 41641;
then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
stop)
log_daemon_msg "Stopping Tailscale VPN" "tailscaled" || true
if start-stop-daemon --stop --remove-pidfile --pidfile /run/tailscaled.pid --exec /usr/sbin/tailscaled; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
status)
status_of_proc -p /run/tailscaled.pid /usr/sbin/tailscaled tailscaled && exit 0 || exit $?
;;
*)
log_action_msg "Usage: /etc/init.d/tailscaled {start|stop|status}" || true
exit 1
esac
exit 0

View File

@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=

11
go.mod
View File

@@ -6,7 +6,7 @@ require (
filippo.io/mkcert v1.4.4
github.com/Microsoft/go-winio v0.6.1
github.com/akutz/memconn v0.1.0
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
github.com/andybalholm/brotli v1.0.5
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aws/aws-sdk-go-v2 v1.21.0
@@ -20,6 +20,7 @@ require (
github.com/creack/pty v1.1.18
github.com/dave/jennifer v1.7.0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.19.4
github.com/frankban/quicktest v1.14.5
@@ -76,14 +77,14 @@ require (
go.uber.org/zap v1.26.0
go4.org/mem v0.0.0-20220726221520-4f986261bf13
go4.org/netipx v0.0.0-20230824141953-6213f710f925
golang.org/x/crypto v0.13.0
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.12.0
golang.org/x/net v0.15.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.12.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.12.0
golang.org/x/term v0.12.0
golang.org/x/sys v0.13.0
golang.org/x/term v0.13.0
golang.org/x/time v0.3.0
golang.org/x/tools v0.13.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2

View File

@@ -1 +1 @@
sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=

22
go.sum
View File

@@ -93,8 +93,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
@@ -233,6 +233,8 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
@@ -984,8 +986,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1080,8 +1082,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1177,8 +1179,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1187,8 +1189,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1 +1 @@
f242beecd311476f6e6b9fa3052e253e2301e170
d1c91593484a1db2d4de2564f2ef2669814af9c8

122
ipn/conf.go Normal file
View File

@@ -0,0 +1,122 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/preftype"
)
// ConfigVAlpha is the config file format for the "alpha0" version.
type ConfigVAlpha struct {
Locked opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true
ServerURL *string `json:",omitempty"` // defaults to https://controlplane.tailscale.com
AuthKey *string `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if it contains a slash)
Enabled opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true
OperatorUser *string `json:",omitempty"` // local user name who is allowed to operate tailscaled without being root or using sudo
Hostname *string `json:",omitempty"`
AcceptDNS opt.Bool `json:"acceptDNS,omitempty"` // --accept-dns
AcceptRoutes opt.Bool `json:"acceptRoutes,omitempty"`
ExitNode *string `json:"exitNode,omitempty"` // IP, StableID, or MagicDNS base name
AllowLANWhileUsingExitNode opt.Bool `json:"allowLANWhileUsingExitNode,omitempty"`
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
DisableSNAT opt.Bool `json:",omitempty"`
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
PostureChecking opt.Bool `json:",omitempty"`
RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH
ShieldsUp opt.Bool `json:",omitempty"`
AutoUpdate *AutoUpdatePrefs `json:",omitempty"`
ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this
// TODO(bradfitz,maisem): future something like:
// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
}
func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
var mp MaskedPrefs
if c == nil {
return mp, nil
}
if c.ServerURL != nil {
mp.ControlURL = *c.ServerURL
mp.ControlURLSet = true
}
if c.Enabled != "" {
mp.WantRunning = c.Enabled.EqualBool(true)
mp.WantRunningSet = true
}
if c.OperatorUser != nil {
mp.OperatorUser = *c.OperatorUser
mp.OperatorUserSet = true
}
if c.Hostname != nil {
mp.Hostname = *c.Hostname
mp.HostnameSet = true
}
if c.AcceptDNS != "" {
mp.CorpDNS = c.AcceptDNS.EqualBool(true)
mp.CorpDNSSet = true
}
if c.AcceptRoutes != "" {
mp.RouteAll = c.AcceptRoutes.EqualBool(true)
mp.RouteAllSet = true
}
if c.ExitNode != nil {
ip, err := netip.ParseAddr(*c.ExitNode)
if err == nil {
mp.ExitNodeIP = ip
mp.ExitNodeIPSet = true
} else {
mp.ExitNodeID = tailcfg.StableNodeID(*c.ExitNode)
mp.ExitNodeIDSet = true
}
}
if c.AllowLANWhileUsingExitNode != "" {
mp.ExitNodeAllowLANAccess = c.AllowLANWhileUsingExitNode.EqualBool(true)
mp.ExitNodeAllowLANAccessSet = true
}
if c.AdvertiseRoutes != nil {
mp.AdvertiseRoutes = c.AdvertiseRoutes
mp.AdvertiseRoutesSet = true
}
if c.DisableSNAT != "" {
mp.NoSNAT = c.DisableSNAT.EqualBool(true)
mp.NoSNAT = true
}
if c.NetfilterMode != nil {
m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
if err != nil {
return mp, err
}
mp.NetfilterMode = m
mp.NetfilterModeSet = true
}
if c.PostureChecking != "" {
mp.PostureChecking = c.PostureChecking.EqualBool(true)
mp.PostureCheckingSet = true
}
if c.RunSSHServer != "" {
mp.RunSSH = c.RunSSHServer.EqualBool(true)
mp.RunSSHSet = true
}
if c.ShieldsUp != "" {
mp.ShieldsUp = c.ShieldsUp.EqualBool(true)
mp.ShieldsUpSet = true
}
if c.AutoUpdate != nil {
mp.AutoUpdate = *c.AutoUpdate
mp.AutoUpdateSet = true
}
return mp, nil
}

66
ipn/conffile/conffile.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package conffile contains code to load, manipulate, and access config file
// settings.
package conffile
import (
"encoding/json"
"fmt"
"os"
"github.com/tailscale/hujson"
"tailscale.com/ipn"
)
// Config describes a config file
type Config struct {
Path string // disk path of HuJSON
Raw []byte // raw bytes from disk, in HuJSON form
Std []byte // standardized JSON form
Version string // "alpha0" for now
// Parsed is the parsed config, converted from its on-disk version to the
// latest known format.
//
// As of 2023-10-15 there is exactly one format ("alpha0") so this is both
// the on-disk format and the in-memory upgraded format.
Parsed ipn.ConfigVAlpha
}
// Load reads and parses the config file at the provided path on disk.
func Load(path string) (*Config, error) {
var c Config
c.Path = path
var err error
c.Raw, err = os.ReadFile(path)
if err != nil {
return nil, err
}
c.Std, err = hujson.Standardize(c.Raw)
if err != nil {
return nil, fmt.Errorf("error parsing config file %s HuJSON/JSON: %w", path, err)
}
var ver struct {
Version string `json:"version"`
}
if err := json.Unmarshal(c.Std, &ver); err != nil {
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
}
switch ver.Version {
case "":
return nil, fmt.Errorf("error parsing config file %s: no \"version\" field defined", path)
case "alpha0":
default:
return nil, fmt.Errorf("error parsing config file %s: unsupported \"version\" value %q; want \"alpha0\" for now", path, ver.Version)
}
c.Version = ver.Version
err = json.Unmarshal(c.Std, &c.Parsed)
if err != nil {
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
}
return &c, nil
}

View File

@@ -52,6 +52,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
PostureChecking bool
Persist *persist.Persist
}{})

21
ipn/ipn_test.go Normal file
View File

@@ -0,0 +1,21 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
"gvisor.dev/gvisor/pkg/tcpip/header": "https://github.com/tailscale/tailscale/issues/9756",
},
}.Check(t)
}

View File

@@ -87,6 +87,7 @@ func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.Netfilte
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -113,6 +114,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
PostureChecking bool
Persist *persist.Persist
}{})

View File

@@ -24,10 +24,12 @@ import (
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/net/sockstats"
"tailscale.com/posture"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
"tailscale.com/util/httpm"
"tailscale.com/util/syspolicy"
"tailscale.com/version"
)
@@ -67,6 +69,14 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
} else {
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
}
case "/posture/identity":
switch r.Method {
case httpm.GET:
b.handleC2NPostureIdentityGet(w, r)
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump(true))
@@ -215,10 +225,42 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques
}()
}
func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /posture/identity received")
res := tailcfg.C2NPostureIdentityResponse{}
// Only collect serial numbers if enabled on the client,
// this will first check syspolicy, MDM settings like Registry
// on Windows or defaults on macOS. If they are not set, it falls
// back to the cli-flag, `--posture-checking`.
choice, err := syspolicy.GetPreferenceOption(syspolicy.PostureChecking)
if err != nil {
b.logf(
"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
b.Prefs().PostureChecking(),
err,
)
}
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
sns, err := posture.GetSerialNumbers(b.logf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res.SerialNumbers = sns
} else {
res.PostureDisabled = true
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// If NewUpdater does not return an error, we can update the installation.
// Exception: When version.IsMacSysExt returns true, we don't support that
// yet. TODO(cpalmer, #6995): Implement it.
//
// Note that we create the Updater solely to check for errors; we do not
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
@@ -226,7 +268,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
_, err := clientupdate.NewUpdater(clientupdate.Arguments{})
return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
Supported: err == nil && !version.IsMacSysExt(),
Supported: err == nil,
}
}

View File

@@ -84,11 +84,9 @@ var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
// ACME process. ACME process is used for new domain certs, existing expired
// certs or existing certs that should get renewed due to upcoming expiry.
//
// syncRenewal changes renewal behavior for existing certs that are still valid
// but need renewal. When syncRenewal is set, the method blocks until a new
// cert is issued. When syncRenewal is not set, existing cert is returned right
// away and renewal is kicked off in a background goroutine.
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
// If a cert is expired, it will be renewed synchronously otherwise it will be
// renewed asynchronously.
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
if !validLookingCertDomain(domain) {
return nil, errors.New("invalid domain")
}
@@ -108,18 +106,16 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
}
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair)
if err != nil {
// If we got here, we have a valid unexpired cert.
// Check whether we should start an async renewal.
if shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair); err != nil {
logf("error checking for certificate renewal: %v", err)
} else if !shouldRenew {
return pair, nil
}
if !syncRenewal {
} else if shouldRenew {
logf("starting async renewal")
// Start renewal in the background.
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
}
// Synchronous renewal happens below.
return pair, nil
}
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
@@ -130,6 +126,8 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewa
return pair, nil
}
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
// based on the current time, the cert's expiry, and the ARI check.
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
renewMu.Lock()
defer renewMu.Unlock()
@@ -365,8 +363,9 @@ type TLSCertKeyPair struct {
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
// getCertPEMCached returns a non-nil keyPair and true if a cached keypair for
// domain exists on disk in dir that is valid at the provided now time.
// getCertPEMCached returns a non-nil keyPair if a cached keypair for domain
// exists on disk in dir that is valid at the provided now time.
//
// If the keypair is expired, it returns errCertExpired.
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {

View File

@@ -12,6 +12,6 @@ type TLSCertKeyPair struct {
CertPEM, KeyPEM []byte
}
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string, syncRenewal bool) (*TLSCertKeyPair, error) {
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
return nil, errors.New("not implemented for js/wasm")
}

View File

@@ -44,6 +44,7 @@ import (
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
@@ -198,7 +199,8 @@ type LocalBackend struct {
// The mutex protects the following elements.
mu sync.Mutex
pm *profileManager // mu guards access
conf *conffile.Config // latest parsed config, or nil if not in declarative mode
pm *profileManager // mu guards access
filterHash deephash.Sum
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
ccGen clientGen // function for producing controlclient; lazily populated
@@ -239,7 +241,6 @@ type LocalBackend struct {
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
incomingFiles map[*incomingFile]bool
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
notifyWatchers set.HandleSet[*watchSession]
lastStatusTime time.Time // status.AsOf value of the last processed status update
@@ -341,6 +342,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
sys: sys,
conf: sys.InitialConfig,
e: e,
dialer: dialer,
store: store,
@@ -519,6 +521,25 @@ func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
b.directFileDoFinalRename = v
}
// ReloadCOnfig reloads the backend's config from disk.
//
// It returns (false, nil) if not running in declarative mode, (true, nil) on
// success, or (false, error) on failure.
func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.conf == nil {
return false, nil
}
conf, err := conffile.Load(b.conf.Path)
if err != nil {
return false, err
}
b.conf = conf
// TODO(bradfitz): apply things
return true, nil
}
// pauseOrResumeControlClientLocked pauses b.cc if there is no network available
// or if the LocalBackend is in Stopped state with a valid NetMap. In all other
// cases, it unpauses it. It is a no-op if b.cc is nil.
@@ -2157,6 +2178,12 @@ func (b *LocalBackend) DebugForceNetmapUpdate() {
b.setNetMapLocked(nm)
}
// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP.
// See its docs.
func (b *LocalBackend) DebugPickNewDERP() error {
return b.sys.MagicSock.Get().DebugPickNewDERP()
}
// send delivers n to the connected frontend and any API watchers from
// LocalBackend.WatchNotifications (via the LocalAPI).
//
@@ -2213,10 +2240,7 @@ func (b *LocalBackend) sendFileNotify() {
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
// in JSON to clients. They distinguish between empty and non-nil
// to know whether a Notify should be able about files.
n.IncomingFiles = make([]ipn.PartialFile, 0)
for f := range b.incomingFiles {
n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
}
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
b.mu.Unlock()
sort.Slice(n.IncomingFiles, func(i, j int) bool {
@@ -3548,12 +3572,13 @@ func (b *LocalBackend) initPeerAPIListener() {
ps := &peerAPIServer{
b: b,
taildrop: &taildrop.Handler{
Logf: b.logf,
Clock: b.clock,
RootDir: fileRoot,
DirectFileMode: b.directFileRoot != "",
DirectFileDoFinalRename: b.directFileDoFinalRename,
taildrop: &taildrop.Manager{
Logf: b.logf,
Clock: tstime.DefaultClock{b.clock},
Dir: fileRoot,
DirectFileMode: b.directFileRoot != "",
AvoidFinalRename: !b.directFileDoFinalRename,
SendFileNotify: b.sendFileNotify,
},
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
@@ -3768,6 +3793,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
hi.RequestTags = prefs.AdvertiseTags().AsSlice()
hi.ShieldsUp = prefs.ShieldsUp()
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply
var sshHostKeys []string
if prefs.RunSSH() && envknob.CanSSHD() {
@@ -4590,19 +4616,6 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
return cc.SetDNS(ctx, req)
}
func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
b.mu.Lock()
defer b.mu.Unlock()
if b.incomingFiles == nil {
b.incomingFiles = make(map[*incomingFile]bool)
}
if active {
b.incomingFiles[inf] = true
} else {
delete(b.incomingFiles, inf)
}
}
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo().Services()
for i := range svcs.LenIter() {

View File

@@ -39,10 +39,9 @@ import (
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/taildrop"
"tailscale.com/tstime"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/version/distro"
"tailscale.com/util/httphdr"
"tailscale.com/wgengine/filter"
)
@@ -56,7 +55,7 @@ type peerAPIServer struct {
b *LocalBackend
resolver *resolver.Resolver
taildrop *taildrop.Handler
taildrop *taildrop.Manager
}
var (
@@ -306,6 +305,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
}
if strings.HasPrefix(r.URL.Path, "/v0/partial-files/") {
h.handlePartialFileGet(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
metricPutCalls.Add(1)
h.handlePeerPut(w, r)
@@ -586,64 +589,6 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "</pre>")
}
type incomingFile struct {
clock tstime.Clock
name string // "foo.jpg"
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
sendFileNotify func() // called when done
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) markAndNotifyDone() {
f.mu.Lock()
f.done = true
f.mu.Unlock()
f.sendFileNotify()
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
var needNotify bool
defer func() {
if needNotify {
f.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := f.clock.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
func (f *incomingFile) PartialFile() ipn.PartialFile {
f.mu.Lock()
defer f.mu.Unlock()
return ipn.PartialFile{
Name: f.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
}
}
// canPutFile reports whether h can put a file ("Taildrop") to this node.
func (h *peerAPIHandler) canPutFile() bool {
if h.peerNode.UnsignedPeerAPIOnly() {
@@ -686,13 +631,71 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if !envknob.CanTaildrop() {
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
var errMisconfiguredInternals = errors.New("misconfigured internals")
func (h *peerAPIHandler) extractBaseName(rawPath, prefix string) (ret string, err error) {
prefix, ok := strings.CutPrefix(rawPath, prefix)
if !ok {
return "", errMisconfiguredInternals
}
if prefix == "" {
return "", taildrop.ErrInvalidFileName
}
if strings.Contains(prefix, "/") {
return "", taildrop.ErrInvalidFileName
}
baseName, err := url.PathUnescape(prefix)
if err == errMisconfiguredInternals {
return "", errMisconfiguredInternals
} else if err != nil {
return "", taildrop.ErrInvalidFileName
}
return baseName, nil
}
func (h *peerAPIHandler) handlePartialFileGet(w http.ResponseWriter, r *http.Request) {
if !h.ps.b.hasCapFileSharing() {
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
return
}
if r.Method != "GET" {
http.Error(w, "expected method GET", http.StatusMethodNotAllowed)
return
}
var resp any
var err error
id := taildrop.ClientID(h.peerNode.StableID())
if r.URL.Path == "/v0/partial-files/" {
resp, err = h.ps.taildrop.PartialFiles(id)
} else {
baseName, _ := h.extractBaseName(r.URL.EscapedPath(), "/v0/partial-files/")
ranges, ok := httphdr.ParseRange(r.Header.Get("Range"))
if !ok || len(ranges) != 1 || ranges[0].Length < 0 {
http.Error(w, "invalid Range header", http.StatusBadRequest)
return
}
offset := ranges[0].Start
length := ranges[0].Length
if length == 0 {
length = -1 // httphdr.Range.Length == 0 implies reading the rest of file
}
resp, err = h.ps.taildrop.HashPartialFile(id, baseName, offset, length)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if !h.canPutFile() {
http.Error(w, "Taildrop access denied", http.StatusForbidden)
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusForbidden)
return
}
if !h.ps.b.hasCapFileSharing() {
@@ -703,113 +706,38 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
return
}
if mayDeref(h.ps.taildrop).RootDir == "" {
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusInternalServerError)
return
}
if distro.Get() == distro.Unraid && !h.ps.taildrop.DirectFileMode {
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
return
}
rawPath := r.URL.EscapedPath()
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
if !ok {
http.Error(w, "misconfigured internals", 500)
return
}
if suffix == "" {
http.Error(w, "empty filename", 400)
return
}
if strings.Contains(suffix, "/") {
http.Error(w, "directories not supported", 400)
return
}
baseName, err := url.PathUnescape(suffix)
baseName, err := h.extractBaseName(r.URL.EscapedPath(), "/v0/put/")
if err != nil {
http.Error(w, "bad path encoding", 400)
return
}
dstFile, ok := h.ps.taildrop.DiskPath(baseName)
if !ok {
http.Error(w, "bad filename", 400)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
t0 := h.ps.b.clock.Now()
// TODO(bradfitz): prevent same filename being sent by two peers at once
id := taildrop.ClientID(h.peerNode.StableID())
// prevent same filename being sent twice
if _, err := os.Stat(dstFile); err == nil {
http.Error(w, "file exists", http.StatusConflict)
return
}
partialFile := dstFile + taildrop.PartialSuffix
f, err := os.Create(partialFile)
if err != nil {
h.logf("put Create error: %v", taildrop.RedactErr(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var success bool
defer func() {
if !success {
os.Remove(partialFile)
}
}()
var finalSize int64
var inFile *incomingFile
if r.ContentLength != 0 {
inFile = &incomingFile{
clock: h.ps.b.clock,
name: baseName,
started: h.ps.b.clock.Now(),
size: r.ContentLength,
w: f,
sendFileNotify: h.ps.b.sendFileNotify,
}
if h.ps.taildrop.DirectFileMode {
inFile.partialPath = partialFile
}
h.ps.b.registerIncomingFile(inFile, true)
defer h.ps.b.registerIncomingFile(inFile, false)
n, err := io.Copy(inFile, r.Body)
if err != nil {
err = taildrop.RedactErr(err)
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
var offset int64
if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
ranges, ok := httphdr.ParseRange(rangeHdr)
if !ok || len(ranges) != 1 || ranges[0].Length != 0 {
http.Error(w, "invalid Range header", http.StatusBadRequest)
return
}
finalSize = n
offset = ranges[0].Start
}
if err := taildrop.RedactErr(f.Close()); err != nil {
h.logf("put Close error: %v", err)
n, err := h.ps.taildrop.PutFile(taildrop.ClientID(fmt.Sprint(id)), baseName, r.Body, offset, r.ContentLength)
switch err {
case nil:
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
io.WriteString(w, "{}\n")
case taildrop.ErrNoTaildrop:
http.Error(w, err.Error(), http.StatusForbidden)
case taildrop.ErrInvalidFileName:
http.Error(w, err.Error(), http.StatusBadRequest)
case taildrop.ErrFileExists:
http.Error(w, err.Error(), http.StatusConflict)
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.taildrop.DirectFileMode && !h.ps.taildrop.DirectFileDoFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
} else {
if err := os.Rename(partialFile, dstFile); err != nil {
err = taildrop.RedactErr(err)
h.logf("put final rename: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
h.ps.taildrop.KnownEmpty.Store(false)
h.ps.b.sendFileNotify()
}
func approxSize(n int64) string {
@@ -882,7 +810,7 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
}
dh := health.DebugHandler("dnsfwd")
if dh == nil {
http.Error(w, "not wired up", 500)
http.Error(w, "not wired up", http.StatusInternalServerError)
return
}
dh.ServeHTTP(w, r)
@@ -1020,9 +948,9 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
if err != nil {
h.logf("handleDNS fwd error: %v", err)
if err := ctx.Err(); err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Error(w, "DNS forwarding error", 500)
http.Error(w, "DNS forwarding error", http.StatusInternalServerError)
}
return
}

View File

@@ -5,7 +5,6 @@ package ipnlocal
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
@@ -15,11 +14,12 @@ import (
"net/netip"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"go4.org/netipx"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
@@ -68,7 +68,7 @@ func bodyNotContains(sub string) check {
func fileHasSize(name string, size int) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.RootDir
root := e.ph.ps.taildrop.Dir
if root == "" {
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
return
@@ -84,7 +84,7 @@ func fileHasSize(name string, size int) check {
func fileHasContents(name string, want string) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.RootDir
root := e.ph.ps.taildrop.Dir
if root == "" {
t.Errorf("no rootdir; can't check contents of %q", name)
return
@@ -173,7 +173,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(http.StatusForbidden),
bodyContains("Taildrop access denied"),
bodyContains("Taildrop disabled"),
),
},
{
@@ -193,7 +193,7 @@ func TestHandlePeerAPI(t *testing.T) {
capSharing: true,
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil)},
checks: checks(
httpStatus(http.StatusInternalServerError),
httpStatus(http.StatusForbidden),
bodyContains("Taildrop disabled; no storage directory"),
),
},
@@ -250,7 +250,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.partial", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -260,7 +260,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -270,7 +270,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/.", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -280,7 +280,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/", nil)},
checks: checks(
httpStatus(400),
bodyContains("empty filename"),
bodyContains("invalid filename"),
),
},
{
@@ -290,7 +290,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo/bar", nil)},
checks: checks(
httpStatus(400),
bodyContains("directories not supported"),
bodyContains("invalid filename"),
),
},
{
@@ -300,7 +300,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -310,7 +310,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -320,7 +320,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -330,7 +330,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -340,7 +340,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -372,7 +372,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -382,7 +382,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%00", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -392,7 +392,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/%01", nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -402,7 +402,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -412,7 +412,7 @@ func TestHandlePeerAPI(t *testing.T) {
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil)},
checks: checks(
httpStatus(400),
bodyContains("bad filename"),
bodyContains("invalid filename"),
),
},
{
@@ -443,23 +443,69 @@ func TestHandlePeerAPI(t *testing.T) {
),
},
{
name: "bad_duplicate_zero_length",
name: "duplicate_zero_length",
isSelf: true,
capSharing: true,
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", nil), httptest.NewRequest("PUT", "/v0/put/foo", nil)},
reqs: []*http.Request{
httptest.NewRequest("PUT", "/v0/put/foo", nil),
httptest.NewRequest("PUT", "/v0/put/foo", nil),
},
checks: checks(
httpStatus(409),
bodyContains("file exists"),
httpStatus(200),
func(t *testing.T, env *peerAPITestEnv) {
got, err := env.ph.ps.taildrop.WaitingFiles()
if err != nil {
t.Fatalf("WaitingFiles error: %v", err)
}
want := []apitype.WaitingFile{{Name: "foo", Size: 0}}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
}
},
),
},
{
name: "bad_duplicate_non_zero_length_content_length",
name: "duplicate_non_zero_length_content_length",
isSelf: true,
capSharing: true,
reqs: []*http.Request{httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")), httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents"))},
reqs: []*http.Request{
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
},
checks: checks(
httpStatus(409),
bodyContains("file exists"),
httpStatus(200),
func(t *testing.T, env *peerAPITestEnv) {
got, err := env.ph.ps.taildrop.WaitingFiles()
if err != nil {
t.Fatalf("WaitingFiles error: %v", err)
}
want := []apitype.WaitingFile{{Name: "foo", Size: 8}}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
}
},
),
},
{
name: "duplicate_different_files",
isSelf: true,
capSharing: true,
reqs: []*http.Request{
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("fizz")),
httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("buzz")),
},
checks: checks(
httpStatus(200),
func(t *testing.T, env *peerAPITestEnv) {
got, err := env.ph.ps.taildrop.WaitingFiles()
if err != nil {
t.Fatalf("WaitingFiles error: %v", err)
}
want := []apitype.WaitingFile{{Name: "foo", Size: 4}, {Name: "foo (1)", Size: 4}}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("WaitingFile mismatch (-got +want):\n%s", diff)
}
},
),
},
}
@@ -494,9 +540,11 @@ func TestHandlePeerAPI(t *testing.T) {
if !tt.omitRoot {
rootDir = t.TempDir()
if e.ph.ps.taildrop == nil {
e.ph.ps.taildrop = &taildrop.Handler{}
e.ph.ps.taildrop = &taildrop.Manager{
Logf: e.logBuf.Logf,
}
}
e.ph.ps.taildrop.RootDir = rootDir
e.ph.ps.taildrop.Dir = rootDir
}
for _, req := range tt.reqs {
e.rr = httptest.NewRecorder()
@@ -535,10 +583,9 @@ func TestFileDeleteRace(t *testing.T) {
capFileSharing: true,
clock: &tstest.Clock{},
},
taildrop: &taildrop.Handler{
Logf: t.Logf,
Clock: &tstest.Clock{},
RootDir: dir,
taildrop: &taildrop.Manager{
Logf: t.Logf,
Dir: dir,
},
}
ph := &peerAPIHandler{
@@ -579,92 +626,6 @@ func TestFileDeleteRace(t *testing.T) {
}
}
// Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir()
ps := &peerAPIServer{
b: &LocalBackend{
logf: t.Logf,
capFileSharing: true,
},
taildrop: &taildrop.Handler{
RootDir: dir,
},
}
nothingWaiting := func() {
t.Helper()
ps.taildrop.KnownEmpty.Store(false)
if ps.taildrop.HasFilesWaiting() {
t.Fatal("unexpected files waiting")
}
}
touch := func(base string) {
t.Helper()
if err := taildrop.TouchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err)
}
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {
t.Errorf("unexpected file in tempdir: %q", fi.Name())
}
}
}
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
wf, err := ps.taildrop.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wf) != 0 {
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
}
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err == nil {
rc.Close()
t.Fatal("unexpected foo.jpg open")
}
wantEmptyTempDir()
// And verify basics still work in non-deleted cases.
touch("foo.jpg")
touch("bar.jpg.deleted")
if wf, err := ps.taildrop.WaitingFiles(); err != nil {
t.Error(err)
} else if len(wf) != 1 {
t.Errorf("WaitingFiles = %d; want 1", len(wf))
} else if wf[0].Name != "foo.jpg" {
t.Errorf("unexpected waiting file %+v", wf[0])
}
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err != nil {
t.Fatal(err)
} else {
rc.Close()
}
}
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
var h peerAPIHandler
@@ -719,67 +680,3 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
}
}
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := taildrop.RedactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := taildrop.RedactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}

View File

@@ -160,8 +160,9 @@ func (s *serveListener) shouldWarnAboutListenError(err error) bool {
return true
}
// handleServeListenersAccept accepts connections for the Listener.
// Calls incoming handler in a new goroutine for each accepted connection.
// handleServeListenersAccept accepts connections for the Listener. It calls the
// handler in a new goroutine for each accepted connection. This is used to
// handle local "tailscale serve" traffic originating from the machine itself.
func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
for {
conn, err := ln.Accept()
@@ -171,7 +172,7 @@ func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr)
if handler == nil {
s.b.logf("serve RST for %v", srcAddr)
s.b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, s.ap.Port())
conn.Close()
continue
}
@@ -325,32 +326,43 @@ func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
return b.setServeConfigLocked(sc, "")
}
// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer
// proxied to the local node over the PeerAPI.
// Target represents the destination HostPort of the conn.
// srcAddr represents the source AddrPort and not that of the ingressPeer.
// getConnOrReset is a callback to get the connection, or reset if the connection
// is no longer available.
// sendRST is a callback to send a TCP RST to the ingressPeer indicating that
// the connection was not accepted.
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
// TODO(maisem,bradfitz): make this not alloc for every conn.
logf := logger.WithPrefix(b.logf, "handleIngress: ")
if !sc.Valid() {
b.logf("localbackend: got ingress conn w/o serveConfig; rejecting")
logf("got ingress conn w/o serveConfig; rejecting")
sendRST()
return
}
if !sc.HasFunnelForTarget(target) {
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
logf("got ingress conn for unconfigured %q; rejecting", target)
sendRST()
return
}
_, port, err := net.SplitHostPort(string(target))
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
logf("got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
logf("got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
@@ -360,7 +372,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
if handler != nil {
c, ok := getConnOrReset()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
return
}
handler(c)
@@ -371,12 +383,13 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
// extend serveHTTPContext or similar.
handler := b.tcpHandlerForServe(dport, srcAddr)
if handler == nil {
logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport)
sendRST()
return
}
c, ok := getConnOrReset()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
logf("getConn didn't complete from %v to port %v", srcAddr, dport)
return
}
handler(c)
@@ -390,13 +403,11 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
b.mu.Unlock()
if !sc.Valid() {
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
return nil
}
tcph, ok := sc.FindTCP(dport)
if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
return nil
}
@@ -440,7 +451,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni, false)
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
@@ -468,7 +479,6 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
}
}
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
return nil
}
@@ -747,7 +757,7 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, hi.ServerName, false)
pair, err := b.GetCertPEM(ctx, hi.ServerName)
if err != nil {
return nil, err
}

View File

@@ -23,7 +23,7 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal handler config wired wrong", 500)
return
}
pair, err := h.b.GetCertPEM(r.Context(), domain, true)
pair, err := h.b.GetCertPEM(r.Context(), domain)
if err != nil {
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
// GetCertPEM (and everywhere) should carry info info to get whether

View File

@@ -37,6 +37,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/taildrop"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/key"
@@ -45,6 +46,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/types/tkatype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/httphdr"
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/osdiag"
@@ -79,6 +81,7 @@ var handler = map[string]localAPIHandler{
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
"debug-capture": (*Handler).serveDebugCapture,
"debug-log": (*Handler).serveDebugLog,
"debug-web-client": (*Handler).serveDebugWebClient,
"derpmap": (*Handler).serveDERPMap,
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
@@ -93,6 +96,7 @@ var handler = map[string]localAPIHandler{
"ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs,
"pprof": (*Handler).servePprof,
"reload-config": (*Handler).reloadConfig,
"reset-auth": (*Handler).serveResetAuth,
"serve-config": (*Handler).serveServeConfig,
"set-dns": (*Handler).serveSetDNS,
@@ -279,23 +283,23 @@ func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
}
b, err := json.Marshal(req)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
httpReq, err := http.NewRequest("POST", "https://unused/machine/id-token", bytes.NewReader(b))
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := h.b.DoNoiseRequest(httpReq)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
@@ -408,36 +412,52 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
h.serveWhoIsWithBackend(w, r, h.b)
}
// localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
// by the localapi WhoIs method.
type localBackendWhoIsMethods interface {
WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
PeerCaps(netip.Addr) tailcfg.PeerCapMap
}
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
b := h.b
var ipp netip.AddrPort
if v := r.FormValue("addr"); v != "" {
var err error
ipp, err = netip.ParseAddrPort(v)
if err != nil {
http.Error(w, "invalid 'addr' parameter", 400)
return
if ip, err := netip.ParseAddr(v); err == nil {
ipp = netip.AddrPortFrom(ip, 0)
} else {
var err error
ipp, err = netip.ParseAddrPort(v)
if err != nil {
http.Error(w, "invalid 'addr' parameter", http.StatusBadRequest)
return
}
}
} else {
http.Error(w, "missing 'addr' parameter", 400)
http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
return
}
n, u, ok := b.WhoIs(ipp)
if !ok {
http.Error(w, "no match for IP:port", 404)
http.Error(w, "no match for IP:port", http.StatusNotFound)
return
}
res := &apitype.WhoIsResponse{
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
UserProfile: &u, // always non-nil per WhoIsResponse contract
CapMap: b.PeerCaps(ipp.Addr()),
}
if n.Addresses().Len() > 0 {
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -566,13 +586,15 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if err == nil {
return
}
case "pick-new-derp":
err = h.b.DebugPickNewDERP()
case "":
err = fmt.Errorf("missing parameter 'action'")
default:
err = fmt.Errorf("unknown action %q", action)
}
if err != nil {
http.Error(w, err.Error(), 400)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/plain")
@@ -589,7 +611,7 @@ func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request)
return
}
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain")
@@ -817,6 +839,26 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
servePprofFunc(w, r)
}
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
ok, err := h.b.ReloadConfig()
var res apitype.ReloadConfigResponse
res.Reloaded = ok
if err != nil {
res.Err = err.Error()
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&res)
}
func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "reset-auth modify access denied", http.StatusForbidden)
@@ -919,18 +961,18 @@ func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.R
ipStr := r.FormValue("ip")
if ipStr == "" {
http.Error(w, "missing 'ip' parameter", 400)
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
return
}
ip, err := netip.ParseAddr(ipStr)
if err != nil {
http.Error(w, "invalid IP", 400)
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -1007,7 +1049,7 @@ func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request)
return
}
if r.Method != "POST" {
http.Error(w, "want POST", 400)
http.Error(w, "want POST", http.StatusBadRequest)
return
}
h.b.StartLoginInteractive()
@@ -1021,7 +1063,7 @@ func (h *Handler) serveStart(w http.ResponseWriter, r *http.Request) {
return
}
if r.Method != "POST" {
http.Error(w, "want POST", 400)
http.Error(w, "want POST", http.StatusBadRequest)
return
}
var o ipn.Options
@@ -1044,7 +1086,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
return
}
if r.Method != "POST" {
http.Error(w, "want POST", 400)
http.Error(w, "want POST", http.StatusBadRequest)
return
}
err := h.b.Logout(r.Context())
@@ -1052,7 +1094,7 @@ func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
return
}
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
@@ -1069,7 +1111,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
}
mp := new(ipn.MaskedPrefs)
if err := json.NewDecoder(r.Body).Decode(mp); err != nil {
http.Error(w, err.Error(), 400)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var err error
@@ -1107,7 +1149,7 @@ func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
}
p := new(ipn.Prefs)
if err := json.NewDecoder(r.Body).Decode(p); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
err := h.b.CheckPrefs(p)
@@ -1131,7 +1173,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
}
if suffix == "" {
if r.Method != "GET" {
http.Error(w, "want GET to list files", 400)
http.Error(w, "want GET to list files", http.StatusBadRequest)
return
}
ctx := r.Context()
@@ -1148,7 +1190,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
}
wfs, err := h.b.AwaitWaitingFiles(ctx)
if err != nil && ctx.Err() == nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1157,12 +1199,12 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
}
name, err := url.PathUnescape(suffix)
if err != nil {
http.Error(w, "bad filename", 400)
http.Error(w, "bad filename", http.StatusBadRequest)
return
}
if r.Method == "DELETE" {
if err := h.b.DeleteFile(name); err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
@@ -1170,7 +1212,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
}
rc, size, err := h.b.OpenFile(name)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rc.Close()
@@ -1184,7 +1226,7 @@ func writeErrorJSON(w http.ResponseWriter, err error) {
err = errors.New("unexpected nil error")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
type E struct {
Error string `json:"error"`
}
@@ -1197,7 +1239,7 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
return
}
if r.Method != "GET" {
http.Error(w, "want GET to list targets", 400)
http.Error(w, "want GET to list targets", http.StatusBadRequest)
return
}
fts, err := h.b.FileTargets()
@@ -1237,12 +1279,12 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
return
}
if r.Method != "PUT" {
http.Error(w, "want PUT to put file", 400)
http.Error(w, "want PUT to put file", http.StatusBadRequest)
return
}
fts, err := h.b.FileTargets()
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -1253,7 +1295,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
}
stableIDStr, filenameEscaped, ok := strings.Cut(upath, "/")
if !ok {
http.Error(w, "bogus URL", 400)
http.Error(w, "bogus URL", http.StatusBadRequest)
return
}
stableID := tailcfg.StableNodeID(stableIDStr)
@@ -1266,20 +1308,60 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
}
}
if ft == nil {
http.Error(w, "node not found", 404)
http.Error(w, "node not found", http.StatusNotFound)
return
}
dstURL, err := url.Parse(ft.PeerAPIURL)
if err != nil {
http.Error(w, "bogus peer URL", 500)
http.Error(w, "bogus peer URL", http.StatusInternalServerError)
return
}
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, r.Body)
// Before we PUT a file we check to see if there are any existing partial file and if so,
// we resume the upload from where we left off by sending the remaining file instead of
// the full file.
offset, remainingBody, err := taildrop.ResumeReader(r.Body, func(offset, length int64) (taildrop.FileChecksums, error) {
client := &http.Client{
Transport: h.b.Dialer().PeerAPITransport(),
Timeout: 10 * time.Second,
}
req, err := http.NewRequestWithContext(r.Context(), "GET", "http://peer/v0/partial-files/"+filenameEscaped, nil)
if err != nil {
return taildrop.FileChecksums{}, err
}
rangeHdr, ok := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: length}})
if !ok {
return taildrop.FileChecksums{}, fmt.Errorf("invalid offset and length")
}
req.Header.Set("Range", rangeHdr)
resp, err := client.Do(req)
if err != nil {
return taildrop.FileChecksums{}, err
}
var checksums taildrop.FileChecksums
err = json.NewDecoder(resp.Body).Decode(&checksums)
return checksums, err
})
if err != nil {
http.Error(w, "bogus outreq", 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, remainingBody)
if err != nil {
http.Error(w, "bogus outreq", http.StatusInternalServerError)
return
}
outReq.ContentLength = r.ContentLength
if offset > 0 {
rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{offset, 0}})
outReq.Header.Set("Range", rangeHdr)
if outReq.ContentLength >= 0 {
outReq.ContentLength -= offset
}
}
rp := httputil.NewSingleHostReverseProxy(dstURL)
rp.Transport = h.b.Dialer().PeerAPITransport()
@@ -1292,7 +1374,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
return
}
if r.Method != "POST" {
http.Error(w, "want POST", 400)
http.Error(w, "want POST", http.StatusBadRequest)
return
}
ctx := r.Context()
@@ -1307,7 +1389,7 @@ func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "want GET", 400)
http.Error(w, "want GET", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1352,22 +1434,22 @@ func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.Method != "POST" {
http.Error(w, "want POST", 400)
http.Error(w, "want POST", http.StatusBadRequest)
return
}
ipStr := r.FormValue("ip")
if ipStr == "" {
http.Error(w, "missing 'ip' parameter", 400)
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
return
}
ip, err := netip.ParseAddr(ipStr)
if err != nil {
http.Error(w, "invalid IP", 400)
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
pingTypeStr := r.FormValue("type")
if pingTypeStr == "" {
http.Error(w, "missing 'type' parameter", 400)
http.Error(w, "missing 'type' parameter", http.StatusBadRequest)
return
}
size := 0
@@ -1375,15 +1457,15 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
if sizeStr != "" {
size, err = strconv.Atoi(sizeStr)
if err != nil {
http.Error(w, "invalid 'size' parameter", 400)
http.Error(w, "invalid 'size' parameter", http.StatusBadRequest)
return
}
if size != 0 && tailcfg.PingType(pingTypeStr) != tailcfg.PingDisco {
http.Error(w, "'size' parameter is only supported with disco pings", 400)
http.Error(w, "'size' parameter is only supported with disco pings", http.StatusBadRequest)
return
}
if size > magicsock.MaxDiscoPingSize {
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), 400)
http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), http.StatusBadRequest)
return
}
}
@@ -1464,7 +1546,7 @@ func (h *Handler) serveSetPushDeviceToken(w http.ResponseWriter, r *http.Request
}
var params apitype.SetPushDeviceTokenRequest
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
hostinfo.SetPushDeviceToken(params.PushDeviceToken)
@@ -1485,7 +1567,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
var clientMetrics []clientMetricJSON
if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
@@ -1497,7 +1579,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
metric.Add(int64(m.Value))
} else {
if clientmetric.HasPublished(m.Name) {
http.Error(w, "Already have a metric named "+m.Name, 400)
http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest)
return
}
var metric *clientmetric.Metric
@@ -1507,7 +1589,7 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
case "gauge":
metric = clientmetric.NewGauge(m.Name)
default:
http.Error(w, "Unknown metric type "+m.Type, 400)
http.Error(w, "Unknown metric type "+m.Type, http.StatusBadRequest)
return
}
metrics[m.Name] = metric
@@ -1531,7 +1613,7 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1583,7 +1665,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
}
var req initRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
@@ -1594,7 +1676,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1617,7 +1699,7 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
}
var req modifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
@@ -1677,14 +1759,14 @@ func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.R
}
var req verifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON for verifyRequest body", 400)
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
return
}
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1704,7 +1786,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
body := io.LimitReader(r.Body, 1024*1024)
secret, err := io.ReadAll(body)
if err != nil {
http.Error(w, "reading secret", 400)
http.Error(w, "reading secret", http.StatusBadRequest)
return
}
@@ -1728,7 +1810,7 @@ func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
// Require a JSON stanza for the body as an additional CSRF protection.
var req struct{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
@@ -1763,7 +1845,7 @@ func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
j, err := json.MarshalIndent(updates, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -1826,7 +1908,7 @@ func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Req
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
if err != nil {
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
@@ -2037,6 +2119,65 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
}
}
// serveDebugWebClient is for use by the web client to communicate with
// the control server for browser auth sessions.
//
// This is an unsupported localapi endpoint and restricted to flagged
// domains on the control side. TODO(tailscale/#14335): Rename this handler.
func (h *Handler) serveDebugWebClient(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
type reqData struct {
ID string
Src tailcfg.NodeID
}
var data reqData
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "invalid JSON body", 400)
return
}
nm := h.b.NetMap()
if nm == nil || !nm.SelfNode.Valid() {
http.Error(w, "[unexpected] no self node", 400)
return
}
dst := nm.SelfNode.ID()
var noiseURL string
if data.ID != "" {
noiseURL = fmt.Sprintf("https://unused/machine/webclient/wait/%d/to/%d/%s", data.Src, dst, data.ID)
} else {
noiseURL = fmt.Sprintf("https://unused/machine/webclient/init/%d/to/%d", data.Src, dst)
}
req, err := http.NewRequestWithContext(r.Context(), "POST", noiseURL, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := h.b.DoNoiseRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if _, err := io.Copy(w, resp.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
}
func defBool(a string, def bool) bool {
if a == "" {
return def
@@ -2081,7 +2222,7 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
var logRequest logRequestJSON
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
http.Error(w, "invalid JSON body", 400)
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}

View File

@@ -9,11 +9,15 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"strings"
"testing"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
)
@@ -77,3 +81,68 @@ func TestSetPushDeviceToken(t *testing.T) {
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
}
}
type whoIsBackend struct {
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
peerCaps map[netip.Addr]tailcfg.PeerCapMap
}
func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
return b.whoIs(ipp)
}
func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
return b.peerCaps[ip]
}
// Tests that the WhoIs handler accepts either IPs or IP:ports.
//
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
func TestWhoIsJustIP(t *testing.T) {
h := &Handler{
PermitRead: true,
}
for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
rec := httptest.NewRecorder()
t.Run(input, func(t *testing.T) {
b := whoIsBackend{
whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
if !strings.Contains(input, ":") {
want := netip.MustParseAddrPort("100.101.102.103:0")
if ipp != want {
t.Fatalf("backend called with %v; want %v", ipp, want)
}
}
return (&tailcfg.Node{
ID: 123,
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.101.102.103/32"),
},
}).View(),
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
true
},
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
"foo": {`"bar"`},
},
},
}
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
var res apitype.WhoIsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
t.Fatal(err)
}
if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
t.Errorf("res.Node.ID=%v, want %v", got, want)
}
if got, want := res.UserProfile.DisplayName, "foo"; got != want {
t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
}
if got, want := len(res.CapMap), 1; got != want {
t.Errorf("capmap size=%v, want %v", got, want)
}
})
}
}

View File

@@ -200,6 +200,10 @@ type Prefs struct {
// AutoUpdatePrefs docs for more details.
AutoUpdate AutoUpdatePrefs
// PostureChecking enables the collection of information used for device
// posture checks.
PostureChecking bool
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
@@ -246,6 +250,7 @@ type MaskedPrefs struct {
OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"`
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
@@ -439,7 +444,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) &&
p.ProfileName == p2.ProfileName &&
p.AutoUpdate == p2.AutoUpdate
p.AutoUpdate == p2.AutoUpdate &&
p.PostureChecking == p2.PostureChecking
}
func (au AutoUpdatePrefs) Pretty() string {

View File

@@ -57,6 +57,7 @@ func TestPrefsEqual(t *testing.T) {
"OperatorUser",
"ProfileName",
"AutoUpdate",
"PostureChecking",
"Persist",
}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
@@ -304,6 +305,16 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
true,
},
{
&Prefs{PostureChecking: true},
&Prefs{PostureChecking: true},
true,
},
{
&Prefs{PostureChecking: true},
&Prefs{PostureChecking: false},
false,
},
}
for i, tt := range tests {
got := tt.a.Equals(tt.b)

View File

@@ -37,6 +37,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.18/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/LICENSE))
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
@@ -85,13 +86,13 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.15.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.17.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.12.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.12.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.12.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.13.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.13.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.13.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))

View File

@@ -265,6 +265,13 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
dbg("nm-safe", "yes")
return "network-manager", nil
}
if err := env.nmIsUsingResolved(); err != nil {
// If systemd-resolved is not running at all, then we don't have any
// other choice: we take direct control of DNS.
dbg("nm-resolved", "no")
return "direct", nil
}
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
dbg("nm-safe", "no")
return "systemd-resolved", nil

View File

@@ -270,6 +270,18 @@ func TestLinuxDNSMode(t *testing.T) {
wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
want: "systemd-resolved",
},
{
// regression test for https://github.com/tailscale/tailscale/issues/9687
name: "networkmanager_endeavouros",
env: env(resolvDotConf(
"# Generated by NetworkManager",
"search example.com localdomain",
"nameserver 10.0.0.1"),
nmRunning("1.44.2", false)),
wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
"dns: [rc=nm resolved=not-in-use ret=direct]",
want: "direct",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -6,6 +6,7 @@
package dns
import (
"bytes"
"os/exec"
)
@@ -13,13 +14,17 @@ func resolvconfStyle() string {
if _, err := exec.LookPath("resolvconf"); err != nil {
return ""
}
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
if err != nil {
// Debian resolvconf doesn't understand --version, and
// exits with a specific error code.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
return "debian"
}
}
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
return "debian"
}
// Treat everything else as openresolv, by far the more popular implementation.
return "openresolv"
}

View File

@@ -39,14 +39,22 @@ import (
"tailscale.com/util/slicesx"
)
var disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER")
var (
optRecursiveResolver = envknob.RegisterOptBool("TS_DNSFALLBACK_RECURSIVE_RESOLVER")
disableRecursiveResolver = envknob.RegisterBool("TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER") // legacy pre-1.52 env knob name
)
// MakeLookupFunc creates a function that can be used to resolve hostnames
// (e.g. as a LookupIPFallback from dnscache.Resolver).
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.Context, host string) ([]netip.Addr, error) {
return func(ctx context.Context, host string) ([]netip.Addr, error) {
if disableRecursiveResolver() {
// If they've explicitly disabled the recursive resolver with the legacy
// TS_DNSFALLBACK_DISABLE_RECURSIVE_RESOLVER envknob or not set the
// newer TS_DNSFALLBACK_RECURSIVE_RESOLVER to true, then don't use the
// recursive resolver. (tailscale/corp#15261) In the future, we might
// change the default (the opt.Bool being unset) to mean enabled.
if disableRecursiveResolver() || !optRecursiveResolver().EqualBool(true) {
return lookup(ctx, host, logf, netMon)
}

View File

@@ -0,0 +1,197 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package checksum provides functions for updating checksums in parsed packets.
package checksum
import (
"encoding/binary"
"net/netip"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
)
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
// is supported. It panics if provided with an address in a different
// family to the parsed packet.
func UpdateSrcAddr(q *packet.Parsed, src netip.Addr) {
if src.Is6() && q.IPVersion != 6 {
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
} else if src.Is4() && q.IPVersion != 4 {
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
}
q.CaptureMeta.DidSNAT = true
q.CaptureMeta.OriginalSrc = q.Src
old := q.Src.Addr()
q.Src = netip.AddrPortFrom(src, q.Src.Port())
b := q.Buffer()
if src.Is6() {
v6 := src.As16()
copy(b[8:24], v6[:])
updateV6PacketChecksums(q, old, src)
} else {
v4 := src.As4()
copy(b[12:16], v4[:])
updateV4PacketChecksums(q, old, src)
}
}
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
// is supported. It panics if provided with an address in a different
// family to the parsed packet.
func UpdateDstAddr(q *packet.Parsed, dst netip.Addr) {
if dst.Is6() && q.IPVersion != 6 {
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
} else if dst.Is4() && q.IPVersion != 4 {
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
}
q.CaptureMeta.DidDNAT = true
q.CaptureMeta.OriginalDst = q.Dst
old := q.Dst.Addr()
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
b := q.Buffer()
if dst.Is6() {
v6 := dst.As16()
copy(b[24:36], v6[:])
updateV6PacketChecksums(q, old, dst)
} else {
v4 := dst.As4()
copy(b[16:20], v4[:])
updateV4PacketChecksums(q, old, dst)
}
}
// updateV4PacketChecksums updates the checksums in the packet buffer.
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
// p is modified in place.
// If p.IPProto is unknown, only the IP header checksum is updated.
func updateV4PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
if len(p.Buffer()) < 12 {
// Not enough space for an IPv4 header.
return
}
o4, n4 := old.As4(), new.As4()
// First update the checksum in the IP header.
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:])
// Now update the transport layer checksums, where applicable.
tr := p.Transport()
switch p.IPProto {
case ipproto.UDP, ipproto.DCCP:
if len(tr) < header.UDPMinimumSize {
// Not enough space for a UDP header.
return
}
updateV4Checksum(tr[6:8], o4[:], n4[:])
case ipproto.TCP:
if len(tr) < header.TCPMinimumSize {
// Not enough space for a TCP header.
return
}
updateV4Checksum(tr[16:18], o4[:], n4[:])
case ipproto.GRE:
if len(tr) < 6 {
// Not enough space for a GRE header.
return
}
if tr[0] == 1 { // checksum present
updateV4Checksum(tr[4:6], o4[:], n4[:])
}
case ipproto.SCTP, ipproto.ICMPv4:
// No transport layer update required.
}
}
// updateV6PacketChecksums updates the checksums in the packet buffer.
// p is modified in place.
// If p.IPProto is unknown, no checksums are updated.
func updateV6PacketChecksums(p *packet.Parsed, old, new netip.Addr) {
if len(p.Buffer()) < 40 {
// Not enough space for an IPv6 header.
return
}
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
// Now update the transport layer checksums, where applicable.
tr := p.Transport()
switch p.IPProto {
case ipproto.ICMPv6:
if len(tr) < header.ICMPv6MinimumSize {
return
}
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
case ipproto.UDP, ipproto.DCCP:
if len(tr) < header.UDPMinimumSize {
return
}
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
case ipproto.TCP:
if len(tr) < header.TCPMinimumSize {
return
}
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
case ipproto.SCTP:
// No transport layer update required.
}
}
// updateV4Checksum calculates and updates the checksum in the packet buffer for
// a change between old and new. The oldSum must point to the 16-bit checksum
// field in the packet buffer that holds the old checksum value, it will be
// updated in place.
//
// The old and new must be the same length, and must be an even number of bytes.
func updateV4Checksum(oldSum, old, new []byte) {
if len(old) != len(new) {
panic("old and new must be the same length")
}
if len(old)%2 != 0 {
panic("old and new must be of even length")
}
/*
RFC 1624
Given the following notation:
HC - old checksum in header
C - one's complement sum of old header
HC' - new checksum in header
C' - one's complement sum of new header
m - old value of a 16-bit field
m' - new value of a 16-bit field
HC' = ~(C + (-m) + m') -- [Eqn. 3]
HC' = ~(~HC + ~m + m')
This can be simplified to:
HC' = ~(C + ~m + m') -- [Eqn. 3]
HC' = ~C'
C' = C + ~m + m'
*/
c := uint32(^binary.BigEndian.Uint16(oldSum))
cPrime := c
for len(new) > 0 {
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
cPrime += mPrime + mNot
new, old = new[2:], old[2:]
}
// Account for overflows by adding the carry bits back into the sum.
for (cPrime >> 16) > 0 {
cPrime = cPrime&0xFFFF + cPrime>>16
}
hcPrime := ^uint16(cPrime)
binary.BigEndian.PutUint16(oldSum, hcPrime)
}

View File

@@ -0,0 +1,196 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package checksum
import (
"encoding/binary"
"net/netip"
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/net/packet"
)
func fullHeaderChecksumV4(b []byte) uint16 {
s := uint32(0)
for i := 0; i < len(b); i += 2 {
if i == 10 {
// Skip checksum field.
continue
}
s += uint32(binary.BigEndian.Uint16(b[i : i+2]))
}
for s>>16 > 0 {
s = s&0xFFFF + s>>16
}
return ^uint16(s)
}
func TestHeaderChecksumsV4(t *testing.T) {
// This is not a good enough test, because it doesn't
// check the various packet types or the many edge cases
// of the checksum algorithm. But it's a start.
tests := []struct {
name string
packet []byte
}{
{
name: "ICMPv4",
packet: []byte{
0x45, 0x00, 0x00, 0x54, 0xb7, 0x96, 0x40, 0x00, 0x40, 0x01, 0x7a, 0x06, 0x64, 0x7f, 0x3f, 0x4c, 0x64, 0x40, 0x01, 0x01, 0x08, 0x00, 0x47, 0x1a, 0x00, 0x11, 0x01, 0xac, 0xcc, 0xf5, 0x95, 0x63, 0x00, 0x00, 0x00, 0x00, 0x8d, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
},
},
{
name: "TLS",
packet: []byte{
0x45, 0x00, 0x00, 0x3c, 0x54, 0x29, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xac, 0x64, 0x42, 0xd4, 0x33, 0x64, 0x61, 0x98, 0x0f, 0xb1, 0x94, 0x01, 0xbb, 0x0a, 0x51, 0xce, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfb, 0xe0, 0x38, 0xf6, 0x00, 0x00, 0x02, 0x04, 0x04, 0xd8, 0x04, 0x02, 0x08, 0x0a, 0x86, 0x2b, 0xcc, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
},
},
{
name: "DNS",
packet: []byte{
0x45, 0x00, 0x00, 0x74, 0xe2, 0x85, 0x00, 0x00, 0x40, 0x11, 0x96, 0xb5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x42, 0xd4, 0x33, 0x00, 0x35, 0xec, 0x55, 0x00, 0x60, 0xd9, 0x19, 0xed, 0xfd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x0c, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x01, 0x6c, 0xc0, 0x15, 0xc0, 0x31, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x04, 0x8e, 0xfa, 0xbd, 0xce, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
name: "DCCP",
packet: []byte{
0x45, 0x00, 0x00, 0x28, 0x15, 0x06, 0x40, 0x00, 0x40, 0x21, 0x5f, 0x2f, 0xc0, 0xa8, 0x01, 0x1f, 0xc9, 0x0b, 0x3b, 0xad, 0x80, 0x04, 0x13, 0x89, 0x05, 0x00, 0x08, 0xdb, 0x01, 0x00, 0x00, 0x04, 0x29, 0x01, 0x6d, 0xdc, 0x00, 0x00, 0x00, 0x00,
},
},
{
name: "SCTP",
packet: []byte{
0x45, 0x00, 0x00, 0x30, 0x09, 0xd9, 0x40, 0x00, 0xff, 0x84, 0x50, 0xe2, 0x0a, 0x1c, 0x06, 0x2c, 0x0a, 0x1c, 0x06, 0x2b, 0x0b, 0x80, 0x40, 0x00, 0x21, 0x44, 0x15, 0x23, 0x2b, 0xf2, 0x02, 0x4e, 0x03, 0x00, 0x00, 0x10, 0x28, 0x02, 0x43, 0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
// TODO(maisem): add test for GRE.
}
var p packet.Parsed
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p.Decode(tt.packet)
t.Log(p.String())
UpdateSrcAddr(&p, netip.MustParseAddr("100.64.0.1"))
got := binary.BigEndian.Uint16(tt.packet[10:12])
want := fullHeaderChecksumV4(tt.packet[:20])
if got != want {
t.Fatalf("got %x want %x", got, want)
}
UpdateDstAddr(&p, netip.MustParseAddr("100.64.0.2"))
got = binary.BigEndian.Uint16(tt.packet[10:12])
want = fullHeaderChecksumV4(tt.packet[:20])
if got != want {
t.Fatalf("got %x want %x", got, want)
}
})
}
}
func TestNatChecksumsV6UDP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
b.Encode(&header.IPv6Fields{
PayloadLength: header.UDPMinimumSize + 32,
TransportProtocol: header.UDPProtocolNumber,
HopLimit: 16,
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
})
udp := header.UDP(b[header.IPv6MinimumSize:])
udp.Encode(&header.UDPFields{
SrcPort: 42,
DstPort: 43,
Length: header.UDPMinimumSize + 32,
})
xsum := header.PseudoHeaderChecksum(
header.UDPProtocolNumber,
tcpip.AddrFrom16Slice(a1.AsSlice()),
tcpip.AddrFrom16Slice(a2.AsSlice()),
uint16(header.UDPMinimumSize+32),
)
xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
udp.SetChecksum(^udp.CalculateChecksum(xsum))
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("test broken; initial packet has incorrect checksum")
}
// Parse the packet.
var p packet.Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
UpdateSrcAddr(&p, a2)
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
UpdateDstAddr(&p, a1)
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating destination address")
}
}
func TestNatChecksumsV6TCP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
// Make a fake TCP packet with no payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
b.Encode(&header.IPv6Fields{
PayloadLength: header.TCPMinimumSize,
TransportProtocol: header.TCPProtocolNumber,
HopLimit: 16,
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
})
tcp := header.TCP(b[header.IPv6MinimumSize:])
tcp.Encode(&header.TCPFields{
SrcPort: 42,
DstPort: 43,
SeqNum: 1,
AckNum: 2,
DataOffset: header.TCPMinimumSize,
Flags: 3,
WindowSize: 4,
Checksum: 0,
UrgentPointer: 5,
})
xsum := header.PseudoHeaderChecksum(
header.TCPProtocolNumber,
tcpip.AddrFrom16Slice(a1.AsSlice()),
tcpip.AddrFrom16Slice(a2.AsSlice()),
uint16(header.TCPMinimumSize),
)
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
t.Fatal("test broken; initial packet has incorrect checksum")
}
// Parse the packet.
var p packet.Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
UpdateSrcAddr(&p, a2)
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
UpdateDstAddr(&p, a1)
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating destination address")
}
}

View File

@@ -10,8 +10,6 @@ import (
"net/netip"
"strings"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/net/netaddr"
"tailscale.com/types/ipproto"
)
@@ -454,62 +452,6 @@ func (q *Parsed) IsEchoResponse() bool {
}
}
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
// SNAT). It also updates the checksum. Currently (2023-09-22) only TCP/UDP/ICMP
// is supported. It panics if provided with an address in a different
// family to the parsed packet.
func (q *Parsed) UpdateSrcAddr(src netip.Addr) {
if src.Is6() && q.IPVersion != 6 {
panic("UpdateSrcAddr: cannot write IPv6 address to v4 packet")
} else if src.Is4() && q.IPVersion != 4 {
panic("UpdateSrcAddr: cannot write IPv4 address to v6 packet")
}
q.CaptureMeta.DidSNAT = true
q.CaptureMeta.OriginalSrc = q.Src
old := q.Src.Addr()
q.Src = netip.AddrPortFrom(src, q.Src.Port())
b := q.Buffer()
if src.Is6() {
v6 := src.As16()
copy(b[8:24], v6[:])
updateV6PacketChecksums(q, old, src)
} else {
v4 := src.As4()
copy(b[12:16], v4[:])
updateV4PacketChecksums(q, old, src)
}
}
// UpdateDstAddr updates the destination address in the packet buffer (e.g. during
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
// is supported. It panics if provided with an address in a different
// family to the parsed packet.
func (q *Parsed) UpdateDstAddr(dst netip.Addr) {
if dst.Is6() && q.IPVersion != 6 {
panic("UpdateDstAddr: cannot write IPv6 address to v4 packet")
} else if dst.Is4() && q.IPVersion != 4 {
panic("UpdateDstAddr: cannot write IPv4 address to v6 packet")
}
q.CaptureMeta.DidDNAT = true
q.CaptureMeta.OriginalDst = q.Dst
old := q.Dst.Addr()
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
b := q.Buffer()
if dst.Is6() {
v6 := dst.As16()
copy(b[24:36], v6[:])
updateV6PacketChecksums(q, old, dst)
} else {
v4 := dst.As4()
copy(b[16:20], v4[:])
updateV4PacketChecksums(q, old, dst)
}
}
// EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response,
// and returns them as a uint32, used to lookup internally routed ICMP echo
// responses. This function is intentionally lightweight as it is called on
@@ -572,129 +514,3 @@ func withIP(ap netip.AddrPort, ip netip.Addr) netip.AddrPort {
func withPort(ap netip.AddrPort, port uint16) netip.AddrPort {
return netip.AddrPortFrom(ap.Addr(), port)
}
// updateV4PacketChecksums updates the checksums in the packet buffer.
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
// p is modified in place.
// If p.IPProto is unknown, only the IP header checksum is updated.
func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) {
if len(p.Buffer()) < 12 {
// Not enough space for an IPv4 header.
return
}
o4, n4 := old.As4(), new.As4()
// First update the checksum in the IP header.
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:])
// Now update the transport layer checksums, where applicable.
tr := p.Transport()
switch p.IPProto {
case ipproto.UDP, ipproto.DCCP:
if len(tr) < header.UDPMinimumSize {
// Not enough space for a UDP header.
return
}
updateV4Checksum(tr[6:8], o4[:], n4[:])
case ipproto.TCP:
if len(tr) < header.TCPMinimumSize {
// Not enough space for a TCP header.
return
}
updateV4Checksum(tr[16:18], o4[:], n4[:])
case ipproto.GRE:
if len(tr) < 6 {
// Not enough space for a GRE header.
return
}
if tr[0] == 1 { // checksum present
updateV4Checksum(tr[4:6], o4[:], n4[:])
}
case ipproto.SCTP, ipproto.ICMPv4:
// No transport layer update required.
}
}
// updateV6PacketChecksums updates the checksums in the packet buffer.
// p is modified in place.
// If p.IPProto is unknown, no checksums are updated.
func updateV6PacketChecksums(p *Parsed, old, new netip.Addr) {
if len(p.Buffer()) < 40 {
// Not enough space for an IPv6 header.
return
}
o6, n6 := tcpip.AddrFrom16Slice(old.AsSlice()), tcpip.AddrFrom16Slice(new.AsSlice())
// Now update the transport layer checksums, where applicable.
tr := p.Transport()
switch p.IPProto {
case ipproto.ICMPv6:
if len(tr) < header.ICMPv6MinimumSize {
return
}
header.ICMPv6(tr).UpdateChecksumPseudoHeaderAddress(o6, n6)
case ipproto.UDP, ipproto.DCCP:
if len(tr) < header.UDPMinimumSize {
return
}
header.UDP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
case ipproto.TCP:
if len(tr) < header.TCPMinimumSize {
return
}
header.TCP(tr).UpdateChecksumPseudoHeaderAddress(o6, n6, true)
case ipproto.SCTP:
// No transport layer update required.
}
}
// updateV4Checksum calculates and updates the checksum in the packet buffer for
// a change between old and new. The oldSum must point to the 16-bit checksum
// field in the packet buffer that holds the old checksum value, it will be
// updated in place.
//
// The old and new must be the same length, and must be an even number of bytes.
func updateV4Checksum(oldSum, old, new []byte) {
if len(old) != len(new) {
panic("old and new must be the same length")
}
if len(old)%2 != 0 {
panic("old and new must be of even length")
}
/*
RFC 1624
Given the following notation:
HC - old checksum in header
C - one's complement sum of old header
HC' - new checksum in header
C' - one's complement sum of new header
m - old value of a 16-bit field
m' - new value of a 16-bit field
HC' = ~(C + (-m) + m') -- [Eqn. 3]
HC' = ~(~HC + ~m + m')
This can be simplified to:
HC' = ~(C + ~m + m') -- [Eqn. 3]
HC' = ~C'
C' = C + ~m + m'
*/
c := uint32(^binary.BigEndian.Uint16(oldSum))
cPrime := c
for len(new) > 0 {
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
cPrime += mPrime + mNot
new, old = new[2:], old[2:]
}
// Account for overflows by adding the carry bits back into the sum.
for (cPrime >> 16) > 0 {
cPrime = cPrime&0xFFFF + cPrime>>16
}
hcPrime := ^uint16(cPrime)
binary.BigEndian.PutUint16(oldSum, hcPrime)
}

View File

@@ -5,7 +5,6 @@ package packet
import (
"bytes"
"encoding/binary"
"encoding/hex"
"net/netip"
"reflect"
@@ -13,9 +12,6 @@ import (
"testing"
"unicode"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"tailscale.com/tstest"
"tailscale.com/types/ipproto"
"tailscale.com/util/must"
@@ -33,187 +29,6 @@ const (
Fragment = ipproto.Fragment
)
func fullHeaderChecksumV4(b []byte) uint16 {
s := uint32(0)
for i := 0; i < len(b); i += 2 {
if i == 10 {
// Skip checksum field.
continue
}
s += uint32(binary.BigEndian.Uint16(b[i : i+2]))
}
for s>>16 > 0 {
s = s&0xFFFF + s>>16
}
return ^uint16(s)
}
func TestHeaderChecksumsV4(t *testing.T) {
// This is not a good enough test, because it doesn't
// check the various packet types or the many edge cases
// of the checksum algorithm. But it's a start.
tests := []struct {
name string
packet []byte
}{
{
name: "ICMPv4",
packet: []byte{
0x45, 0x00, 0x00, 0x54, 0xb7, 0x96, 0x40, 0x00, 0x40, 0x01, 0x7a, 0x06, 0x64, 0x7f, 0x3f, 0x4c, 0x64, 0x40, 0x01, 0x01, 0x08, 0x00, 0x47, 0x1a, 0x00, 0x11, 0x01, 0xac, 0xcc, 0xf5, 0x95, 0x63, 0x00, 0x00, 0x00, 0x00, 0x8d, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
},
},
{
name: "TLS",
packet: []byte{
0x45, 0x00, 0x00, 0x3c, 0x54, 0x29, 0x40, 0x00, 0x40, 0x06, 0xb1, 0xac, 0x64, 0x42, 0xd4, 0x33, 0x64, 0x61, 0x98, 0x0f, 0xb1, 0x94, 0x01, 0xbb, 0x0a, 0x51, 0xce, 0x7c, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfb, 0xe0, 0x38, 0xf6, 0x00, 0x00, 0x02, 0x04, 0x04, 0xd8, 0x04, 0x02, 0x08, 0x0a, 0x86, 0x2b, 0xcc, 0xd5, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
},
},
{
name: "DNS",
packet: []byte{
0x45, 0x00, 0x00, 0x74, 0xe2, 0x85, 0x00, 0x00, 0x40, 0x11, 0x96, 0xb5, 0x64, 0x64, 0x64, 0x64, 0x64, 0x42, 0xd4, 0x33, 0x00, 0x35, 0xec, 0x55, 0x00, 0x60, 0xd9, 0x19, 0xed, 0xfd, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x34, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x0c, 0x07, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x01, 0x6c, 0xc0, 0x15, 0xc0, 0x31, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x1e, 0x00, 0x04, 0x8e, 0xfa, 0xbd, 0xce, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
name: "DCCP",
packet: []byte{
0x45, 0x00, 0x00, 0x28, 0x15, 0x06, 0x40, 0x00, 0x40, 0x21, 0x5f, 0x2f, 0xc0, 0xa8, 0x01, 0x1f, 0xc9, 0x0b, 0x3b, 0xad, 0x80, 0x04, 0x13, 0x89, 0x05, 0x00, 0x08, 0xdb, 0x01, 0x00, 0x00, 0x04, 0x29, 0x01, 0x6d, 0xdc, 0x00, 0x00, 0x00, 0x00,
},
},
{
name: "SCTP",
packet: []byte{
0x45, 0x00, 0x00, 0x30, 0x09, 0xd9, 0x40, 0x00, 0xff, 0x84, 0x50, 0xe2, 0x0a, 0x1c, 0x06, 0x2c, 0x0a, 0x1c, 0x06, 0x2b, 0x0b, 0x80, 0x40, 0x00, 0x21, 0x44, 0x15, 0x23, 0x2b, 0xf2, 0x02, 0x4e, 0x03, 0x00, 0x00, 0x10, 0x28, 0x02, 0x43, 0x45, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
// TODO(maisem): add test for GRE.
}
var p Parsed
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p.Decode(tt.packet)
t.Log(p.String())
p.UpdateSrcAddr(netip.MustParseAddr("100.64.0.1"))
got := binary.BigEndian.Uint16(tt.packet[10:12])
want := fullHeaderChecksumV4(tt.packet[:20])
if got != want {
t.Fatalf("got %x want %x", got, want)
}
p.UpdateDstAddr(netip.MustParseAddr("100.64.0.2"))
got = binary.BigEndian.Uint16(tt.packet[10:12])
want = fullHeaderChecksumV4(tt.packet[:20])
if got != want {
t.Fatalf("got %x want %x", got, want)
}
})
}
}
func TestNatChecksumsV6UDP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
// Make a fake UDP packet with 32 bytes of zeros as the datagram payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.UDPMinimumSize+32))
b.Encode(&header.IPv6Fields{
PayloadLength: header.UDPMinimumSize + 32,
TransportProtocol: header.UDPProtocolNumber,
HopLimit: 16,
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
})
udp := header.UDP(b[header.IPv6MinimumSize:])
udp.Encode(&header.UDPFields{
SrcPort: 42,
DstPort: 43,
Length: header.UDPMinimumSize + 32,
})
xsum := header.PseudoHeaderChecksum(
header.UDPProtocolNumber,
tcpip.AddrFrom16Slice(a1.AsSlice()),
tcpip.AddrFrom16Slice(a2.AsSlice()),
uint16(header.UDPMinimumSize+32),
)
xsum = checksum.Checksum(b.Payload()[header.UDPMinimumSize:], xsum)
udp.SetChecksum(^udp.CalculateChecksum(xsum))
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("test broken; initial packet has incorrect checksum")
}
// Parse the packet.
var p Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
p.UpdateSrcAddr(a2)
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
p.UpdateDstAddr(a1)
if !udp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), checksum.Checksum(b.Payload()[header.UDPMinimumSize:], 0)) {
t.Fatal("incorrect checksum after updating destination address")
}
}
func TestNatChecksumsV6TCP(t *testing.T) {
a1, a2 := netip.MustParseAddr("a::1"), netip.MustParseAddr("b::1")
// Make a fake TCP packet with no payload.
b := header.IPv6(make([]byte, header.IPv6MinimumSize+header.TCPMinimumSize))
b.Encode(&header.IPv6Fields{
PayloadLength: header.TCPMinimumSize,
TransportProtocol: header.TCPProtocolNumber,
HopLimit: 16,
SrcAddr: tcpip.AddrFrom16Slice(a1.AsSlice()),
DstAddr: tcpip.AddrFrom16Slice(a2.AsSlice()),
})
tcp := header.TCP(b[header.IPv6MinimumSize:])
tcp.Encode(&header.TCPFields{
SrcPort: 42,
DstPort: 43,
SeqNum: 1,
AckNum: 2,
DataOffset: header.TCPMinimumSize,
Flags: 3,
WindowSize: 4,
Checksum: 0,
UrgentPointer: 5,
})
xsum := header.PseudoHeaderChecksum(
header.TCPProtocolNumber,
tcpip.AddrFrom16Slice(a1.AsSlice()),
tcpip.AddrFrom16Slice(a2.AsSlice()),
uint16(header.TCPMinimumSize),
)
tcp.SetChecksum(^tcp.CalculateChecksum(xsum))
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a1.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
t.Fatal("test broken; initial packet has incorrect checksum")
}
// Parse the packet.
var p Parsed
p.Decode(b)
t.Log(p.String())
// Update the source address of the packet to be the same as the dest.
p.UpdateSrcAddr(a2)
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a2.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating source address")
}
// Update the dest address of the packet to be the original source address.
p.UpdateDstAddr(a1)
if !tcp.IsChecksumValid(tcpip.AddrFrom16Slice(a2.AsSlice()), tcpip.AddrFrom16Slice(a1.AsSlice()), 0, 0) {
t.Fatal("incorrect checksum after updating destination address")
}
}
func mustIPPort(s string) netip.AddrPort {
ipp, err := netip.ParseAddrPort(s)
if err != nil {

View File

@@ -55,3 +55,4 @@ func (t *fakeTUN) MTU() (int, error) { return 1500, nil }
func (t *fakeTUN) Name() (string, error) { return FakeTUNName, nil }
func (t *fakeTUN) Events() <-chan tun.Event { return t.evchan }
func (t *fakeTUN) BatchSize() int { return 1 }
func (t *fakeTUN) IsFakeTun() bool { return true }

View File

@@ -79,14 +79,16 @@ const (
safeTUNMTU TUNMTU = 1280
)
// MaxProbedWireMTU is the largest MTU we will test for path MTU
// discovery.
var MaxProbedWireMTU WireMTU = 9000
func init() {
if MaxProbedWireMTU > WireMTU(maxTUNMTU) {
MaxProbedWireMTU = WireMTU(maxTUNMTU)
}
// WireMTUsToProbe is a list of the on-the-wire MTUs we want to probe. Each time
// magicsock discovery begins, it will send a set of pings, one of each size
// listed below.
var WireMTUsToProbe = []WireMTU{
WireMTU(safeTUNMTU), // Tailscale over Tailscale :)
TUNToWireMTU(safeTUNMTU), // Smallest MTU allowed for IPv6, current default
1400, // Most common MTU minus a few bytes for tunnels
1500, // Most common MTU
8000, // Should fit inside all jumbo frame sizes
9000, // Most jumbo frames are this size or larger
}
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
@@ -125,7 +127,7 @@ func WireToTUNMTU(w WireMTU) TUNMTU {
// MTU. It is also the path MTU that we default to if we have no
// information about the path to a peer.
//
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTunMTU
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTUNMTU
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg overhead
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
func DefaultTUNMTU() TUNMTU {
@@ -135,12 +137,23 @@ func DefaultTUNMTU() TUNMTU {
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
if debugPMTUD {
return WireToTUNMTU(MaxProbedWireMTU)
// TODO: While we are just probing MTU but not generating PTB,
// this has to continue to return the safe MTU. When we add the
// code to generate PTB, this will be:
//
// return WireToTUNMTU(maxProbedWireMTU)
return safeTUNMTU
}
return safeTUNMTU
}
// SafeWireMTU returns the wire MTU that is safe to use if we have no
// information about the path MTU to this peer.
func SafeWireMTU() WireMTU {
return TUNToWireMTU(safeTUNMTU)
}
// DefaultWireMTU returns the default TUN MTU, adjusted for wireguard
// overhead.
func DefaultWireMTU() WireMTU {

View File

@@ -39,15 +39,18 @@ func TestDefaultTunMTU(t *testing.T) {
t.Errorf("default TUN MTU = %d, want %d, clamping failed", DefaultTUNMTU(), maxTUNMTU)
}
// If PMTUD is enabled, the MTU should default to the largest probed
// MTU, but only if the user hasn't requested a specific MTU.
// If PMTUD is enabled, the MTU should default to the safe MTU, but only
// if the user hasn't requested a specific MTU.
//
// TODO: When PMTUD is generating PTB responses, this will become the
// largest MTU we probe.
os.Setenv("TS_DEBUG_MTU", "")
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
if DefaultTUNMTU() != safeTUNMTU {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
}
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
mtu = WireToTUNMTU(MaxPacketSize - 1)
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
if DefaultTUNMTU() != mtu {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)

View File

@@ -25,6 +25,7 @@ import (
"tailscale.com/disco"
"tailscale.com/net/connstats"
"tailscale.com/net/packet"
"tailscale.com/net/packet/checksum"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun/table"
"tailscale.com/syncs"
@@ -77,6 +78,9 @@ var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
// Wrapper augments a tun.Device with packet filtering and injection.
//
// A Wrapper starts in a "corked" mode where Read calls are blocked
// until the Wrapper's Start method is called.
type Wrapper struct {
logf logger.Logf
limitedLogf logger.Logf // aggressively rate-limited logf used for potentially high volume errors
@@ -84,6 +88,9 @@ type Wrapper struct {
tdev tun.Device
isTAP bool // whether tdev is a TAP device
started atomic.Bool // whether Start has been called
startCh chan struct{} // closed in Start
closeOnce sync.Once
// lastActivityAtomic is read/written atomically.
@@ -218,6 +225,16 @@ type setWrapperer interface {
setWrapper(*Wrapper)
}
// Start unblocks any Wrapper.Read calls that have already started
// and makes the Wrapper functional.
//
// Start must be called exactly once after the various Tailscale
// subsystems have been wired up to each other.
func (w *Wrapper) Start() {
w.started.Store(true)
close(w.startCh)
}
func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper {
return wrap(logf, tdev, true)
}
@@ -243,6 +260,7 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
eventsOther: make(chan tun.Event),
// TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets.
filterFlags: filter.LogAccepts | filter.LogDrops,
startCh: make(chan struct{}),
}
w.vectorBuffer = make([][]byte, tdev.BatchSize())
@@ -308,6 +326,9 @@ func (t *Wrapper) isSelfDisco(p *packet.Parsed) bool {
func (t *Wrapper) Close() error {
var err error
t.closeOnce.Do(func() {
if t.started.CompareAndSwap(false, true) {
close(t.startCh)
}
close(t.closed)
t.bufferConsumedMu.Lock()
t.bufferConsumedClosed = true
@@ -487,7 +508,7 @@ func (t *Wrapper) snat(p *packet.Parsed) {
oldSrc := p.Src.Addr()
newSrc := nc.selectSrcIP(oldSrc, p.Dst.Addr())
if oldSrc != newSrc {
p.UpdateSrcAddr(newSrc)
checksum.UpdateSrcAddr(p, newSrc)
}
}
@@ -497,7 +518,7 @@ func (t *Wrapper) dnat(p *packet.Parsed) {
oldDst := p.Dst.Addr()
newDst := nc.mapDstIP(oldDst)
if newDst != oldDst {
p.UpdateDstAddr(newDst)
checksum.UpdateDstAddr(p, newDst)
}
}
@@ -673,16 +694,16 @@ func (c *natFamilyConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
// natConfigFromWGConfig generates a natFamilyConfig from nm,
// for the indicated address family.
// If NAT is not required for that address family, it returns nil.
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *natFamilyConfig {
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFamilyConfig {
if wcfg == nil {
return nil
}
var nativeAddr netip.Addr
switch addrFam {
case ipproto.IPProtoVersion4:
case ipproto.Version4:
nativeAddr = findV4(wcfg.Addresses)
case ipproto.IPProtoVersion6:
case ipproto.Version6:
nativeAddr = findV6(wcfg.Addresses)
}
if !nativeAddr.IsValid() {
@@ -703,8 +724,8 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
if isExitNode {
hasMasqAddrsForFamily := false ||
(addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
(addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
(addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
(addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
if hasMasqAddrsForFamily {
exitNodeRequiresMasq = true
}
@@ -714,10 +735,10 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
for i := range wcfg.Peers {
p := &wcfg.Peers[i]
var addrToUse netip.Addr
if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
if addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
addrToUse = *p.V4MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
} else if addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
addrToUse = *p.V6MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if exitNodeRequiresMasq {
@@ -741,7 +762,7 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
// SetNetMap is called when a new NetworkMap is received.
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.Version4), natConfigFromWGConfig(wcfg, ipproto.Version6)
var cfg *natConfig
if v4 != nil || v6 != nil {
cfg = &natConfig{v4: v4, v6: v6}
@@ -835,6 +856,9 @@ func (t *Wrapper) IdleDuration() time.Duration {
}
func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
if !t.started.Load() {
<-t.startCh
}
// packet from OS read and sent to WG
res, ok := <-t.vectorOutbound
if !ok {

View File

@@ -178,6 +178,7 @@ func newChannelTUN(logf logger.Logf, secure bool) (*tuntest.ChannelTUN, *Wrapper
} else {
tun.disableFilter = true
}
tun.Start()
return chtun, tun
}
@@ -617,7 +618,7 @@ func TestNATCfg(t *testing.T) {
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
return p
}
test := func(addrFam ipproto.IPProtoVersion) {
test := func(addrFam ipproto.Version) {
var (
noIP netip.Addr
@@ -635,7 +636,7 @@ func TestNATCfg(t *testing.T) {
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
publicIP = netip.MustParseAddr("8.8.8.8")
)
if addrFam == ipproto.IPProtoVersion6 {
if addrFam == ipproto.Version6 {
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
@@ -817,8 +818,8 @@ func TestNATCfg(t *testing.T) {
})
}
}
test(ipproto.IPProtoVersion4)
test(ipproto.IPProtoVersion6)
test(ipproto.Version4)
test(ipproto.Version6)
}
// TestCaptureHook verifies that the Wrapper.captureHook callback is called

View File

@@ -0,0 +1,74 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build cgo && darwin && !ios
package posture
// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
// #include <CoreFoundation/CoreFoundation.h>
// #include <IOKit/IOKitLib.h>
//
// #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
// #define kIOMainPortDefault kIOMasterPortDefault
// #endif
//
// const char *
// getSerialNumber()
// {
// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice");
// if (!matching) {
// return "err: failed to create dictionary to match IOServices";
// }
//
// io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching);
// if (!service) {
// return "err: failed to look up registered IOService objects that match a matching dictionary";
// }
//
// CFStringRef serialNumberRef = IORegistryEntryCreateCFProperty(
// service,
// CFSTR("IOPlatformSerialNumber"),
// kCFAllocatorDefault,
// 0
// );
// if (!serialNumberRef) {
// return "err: failed to look up serial number in IORegistry";
// }
//
// CFIndex length = CFStringGetLength(serialNumberRef);
// CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
// char *serialNumberBuf = (char *)malloc(max_size);
//
// bool result = CFStringGetCString(serialNumberRef, serialNumberBuf, max_size, kCFStringEncodingUTF8);
//
// CFRelease(serialNumberRef);
// IOObjectRelease(service);
//
// if (!result) {
// free(serialNumberBuf);
//
// return "err: failed to convert serial number reference to string";
// }
//
// return serialNumberBuf;
// }
import "C"
import (
"fmt"
"strings"
"tailscale.com/types/logger"
)
// GetSerialNumber returns the platform serial sumber as reported by IOKit.
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
csn := C.getSerialNumber()
serialNumber := C.GoString(csn)
if err, ok := strings.CutPrefix(serialNumber, "err: "); ok {
return nil, fmt.Errorf("failed to get serial number from IOKit: %s", err)
}
return []string{serialNumber}, nil
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build cgo && darwin && !ios
package posture
import (
"fmt"
"testing"
"tailscale.com/types/logger"
"tailscale.com/util/cibuild"
)
func TestGetSerialNumberMac(t *testing.T) {
// Do not run this test on CI, it can only be ran on macOS
// and we currenty only use Linux runners.
if cibuild.On() {
t.Skip()
}
sns, err := GetSerialNumbers(logger.Discard)
if err != nil {
t.Fatalf("failed to get serial number: %s", err)
}
if len(sns) != 1 {
t.Errorf("expected list of one serial number, got %v", sns)
}
if len(sns[0]) <= 0 {
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
}
fmt.Printf("serials: %v\n", sns)
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"errors"
"fmt"
"strings"
"github.com/digitalocean/go-smbios/smbios"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
// the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
// so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
index := specOffset - 4
if index >= len(s.Formatted) || index < 0 {
return 0
}
return s.Formatted[index]
}
// getStringFromSmbiosStructure retrieves a string at the given specOffset.
// Returns an empty string if no string was present.
func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) {
index := getByteFromSmbiosStructure(s, specOffset)
if index == 0 || int(index) > len(s.Strings) {
return "", errors.New("specified offset does not exist in smbios structure")
}
str := s.Strings[index-1]
trimmed := strings.TrimSpace(str)
return trimmed, nil
}
// Product Table (Type 1) structure
// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
// Page 34 and onwards.
const (
// Serial is present at the same offset in all IDs
serialNumberOffset = 0x07
productID = 1
baseboardID = 2
chassisID = 3
)
var (
idToTableName = map[int]string{
1: "product",
2: "baseboard",
3: "chassis",
}
validTables []string
numOfTables int
)
func init() {
for _, table := range idToTableName {
validTables = append(validTables, table)
}
numOfTables = len(validTables)
}
// serialFromSmbiosStructure extracts a serial number from a product,
// baseboard or chassis SMBIOS table.
func serialFromSmbiosStructure(s *smbios.Structure) (string, error) {
id := s.Header.Type
if (id != productID) && (id != baseboardID) && (id != chassisID) {
return "", fmt.Errorf(
"cannot get serial table type %d, supported tables are %v",
id,
validTables,
)
}
serial, err := getStringFromSmbiosStructure(s, serialNumberOffset)
if err != nil {
return "", fmt.Errorf(
"failed to get serial from %s table: %w",
idToTableName[int(s.Header.Type)],
err,
)
}
return serial, nil
}
func GetSerialNumbers(logf logger.Logf) ([]string, error) {
// Find SMBIOS data in operating system-specific location.
rc, _, err := smbios.Stream()
if err != nil {
return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
}
defer rc.Close()
// Decode SMBIOS structures from the stream.
d := smbios.NewDecoder(rc)
ss, err := d.Decode()
if err != nil {
return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
}
serials := make([]string, 0, numOfTables)
errs := make([]error, 0, numOfTables)
for _, s := range ss {
switch s.Header.Type {
case productID, baseboardID, chassisID:
serial, err := serialFromSmbiosStructure(s)
if err != nil {
errs = append(errs, err)
continue
}
serials = append(serials, serial)
}
}
err = multierr.New(errs...)
// if there were no serial numbers, check if any errors were
// returned and combine them.
if len(serials) == 0 && err != nil {
return nil, err
}
logf("got serial numbers %v (errors: %s)", serials, err)
return serials, nil
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"fmt"
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumberNotMac(t *testing.T) {
// This test is intentionally skipped as it will
// require root on Linux to get access to the serials.
// The test case is intended for local testing.
// Comment out skip for local testing.
t.Skip()
sns, err := GetSerialNumbers(logger.Discard)
if err != nil {
t.Fatalf("failed to get serial number: %s", err)
}
if len(sns) == 0 {
t.Fatalf("expected at least one serial number, got %v", sns)
}
if len(sns[0]) <= 0 {
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
}
fmt.Printf("serials: %v\n", sns)
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// ios: Apple does not allow getting serials on iOS
// android: not implemented
// js: not implemented
// plan9: not implemented
// solaris: currently unsupported by go-smbios:
// https://github.com/digitalocean/go-smbios/pull/21
//go:build ios || android || solaris || plan9 || js || wasm || (darwin && !cgo)
package posture
import (
"errors"
"tailscale.com/types/logger"
)
// GetSerialNumber returns client machine serial number(s).
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
return nil, errors.New("not implemented")
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package posture
import (
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumber(t *testing.T) {
// ensure GetSerialNumbers is implemented
// or covered by a stub on a given platform.
_, _ = GetSerialNumbers(logger.Discard)
}

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
# nix-direnv cache busting line: sha256-tzMLCNvIjG5e2aslmMt8GWgnfImd0J2a11xutOe59Ss=

View File

@@ -192,6 +192,26 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
return actual, loaded
}
// LoadOrInit returns the value for the given key if it exists
// otherwise f is called to construct the value to be set.
// The lock is held for the duration to prevent duplicate initialization.
func (m *Map[K, V]) LoadOrInit(key K, f func() V) (actual V, loaded bool) {
if actual, loaded := m.Load(key); loaded {
return actual, loaded
}
m.mu.Lock()
defer m.mu.Unlock()
if actual, loaded = m.m[key]; loaded {
return actual, loaded
}
loaded = false
actual = f()
mak.Set(&m.m, key, actual)
return actual, loaded
}
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
m.mu.Lock()
defer m.mu.Unlock()

View File

@@ -91,8 +91,11 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadOrStore("two", 2); v != 2 || ok {
t.Errorf(`LoadOrStore("two", 2) = (%v, %v), want (2, false)`, v, ok)
}
if v, ok := m.LoadOrInit("three", func() int { return 3 }); v != 3 || ok {
t.Errorf(`LoadOrInit("three", 3) = (%v, %v), want (3, true)`, v, ok)
}
got := map[string]int{}
want := map[string]int{"one": 1, "two": 2}
want := map[string]int{"one": 1, "two": 2, "three": 3}
m.Range(func(k string, v int) bool {
got[k] = v
return true
@@ -106,6 +109,7 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadAndDelete("two"); v != 0 || ok {
t.Errorf(`LoadAndDelete("two) = (%v, %v), want (0, false)`, v, ok)
}
m.Delete("three")
m.Delete("one")
m.Delete("noexist")
got = map[string]int{}

View File

@@ -52,3 +52,15 @@ type C2NUpdateResponse struct {
// Started indicates whether the update has started.
Started bool
}
// C2NPostureIdentityResponse contains either a set of identifying serial number
// from the client or a boolean indicating that the machine has opted out of
// posture collection.
type C2NPostureIdentityResponse struct {
// SerialNumbers is a list of serial numbers of the client machine.
SerialNumbers []string `json:",omitempty"`
// PostureDisabled indicates if the machine has opted out of
// device posture collection.
PostureDisabled bool `json:",omitempty"`
}

187
tailcfg/proto_port_range.go Normal file
View File

@@ -0,0 +1,187 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailcfg
import (
"errors"
"fmt"
"strconv"
"strings"
"tailscale.com/types/ipproto"
"tailscale.com/util/vizerror"
)
var (
errEmptyProtocol = errors.New("empty protocol")
errEmptyString = errors.New("empty string")
)
// ProtoPortRange is used to encode "proto:port" format.
// The following formats are supported:
//
// "*" allows all TCP, UDP and ICMP traffic on all ports.
// "<ports>" allows all TCP, UDP and ICMP traffic on the specified ports.
// "proto:*" allows traffic of the specified proto on all ports.
// "proto:<port>" allows traffic of the specified proto on the specified port.
//
// Ports are either a single port number or a range of ports (e.g. "80-90").
// String named protocols support names that ipproto.Proto accepts.
type ProtoPortRange struct {
// Proto is the IP protocol number.
// If Proto is 0, it means TCP+UDP+ICMP(4+6).
Proto int
Ports PortRange
}
// UnmarshalText implements the encoding.TextUnmarshaler interface. See
// ProtoPortRange for the format.
func (ppr *ProtoPortRange) UnmarshalText(text []byte) error {
ppr2, err := parseProtoPortRange(string(text))
if err != nil {
return err
}
*ppr = *ppr2
return nil
}
// MarshalText implements the encoding.TextMarshaler interface. See
// ProtoPortRange for the format.
func (ppr *ProtoPortRange) MarshalText() ([]byte, error) {
if ppr.Proto == 0 && ppr.Ports == (PortRange{}) {
return []byte{}, nil
}
return []byte(ppr.String()), nil
}
// String implements the stringer interface. See ProtoPortRange for the
// format.
func (ppr ProtoPortRange) String() string {
if ppr.Proto == 0 {
if ppr.Ports == PortRangeAny {
return "*"
}
}
var buf strings.Builder
if ppr.Proto != 0 {
// Proto.MarshalText is infallible.
text, _ := ipproto.Proto(ppr.Proto).MarshalText()
buf.Write(text)
buf.Write([]byte(":"))
}
pr := ppr.Ports
if pr.First == pr.Last {
fmt.Fprintf(&buf, "%d", pr.First)
} else if pr == PortRangeAny {
buf.WriteByte('*')
} else {
fmt.Fprintf(&buf, "%d-%d", pr.First, pr.Last)
}
return buf.String()
}
// ParseProtoPortRanges parses a slice of IP port range fields.
func ParseProtoPortRanges(ips []string) ([]ProtoPortRange, error) {
var out []ProtoPortRange
for _, p := range ips {
ppr, err := parseProtoPortRange(p)
if err != nil {
return nil, err
}
out = append(out, *ppr)
}
return out, nil
}
func parseProtoPortRange(ipProtoPort string) (*ProtoPortRange, error) {
if ipProtoPort == "" {
return nil, errEmptyString
}
if ipProtoPort == "*" {
return &ProtoPortRange{Ports: PortRangeAny}, nil
}
if !strings.Contains(ipProtoPort, ":") {
ipProtoPort = "*:" + ipProtoPort
}
protoStr, portRange, err := parseHostPortRange(ipProtoPort)
if err != nil {
return nil, err
}
if protoStr == "" {
return nil, errEmptyProtocol
}
ppr := &ProtoPortRange{
Ports: portRange,
}
if protoStr == "*" {
return ppr, nil
}
var ipProto ipproto.Proto
if err := ipProto.UnmarshalText([]byte(protoStr)); err != nil {
return nil, err
}
ppr.Proto = int(ipProto)
return ppr, nil
}
// parseHostPortRange parses hostport as HOST:PORTS where HOST is
// returned unchanged and PORTS is is either "*" or PORTLOW-PORTHIGH ranges.
func parseHostPortRange(hostport string) (host string, ports PortRange, err error) {
hostport = strings.ToLower(hostport)
colon := strings.LastIndexByte(hostport, ':')
if colon < 0 {
return "", ports, vizerror.New("hostport must contain a colon (\":\")")
}
host = hostport[:colon]
portlist := hostport[colon+1:]
if strings.Contains(host, ",") {
return "", ports, vizerror.New("host cannot contain a comma (\",\")")
}
if portlist == "*" {
// Special case: permit hostname:* as a port wildcard.
return host, PortRangeAny, nil
}
if len(portlist) == 0 {
return "", ports, vizerror.Errorf("invalid port list: %#v", portlist)
}
if strings.Count(portlist, "-") > 1 {
return "", ports, vizerror.Errorf("port range %#v: too many dashes(-)", portlist)
}
firstStr, lastStr, isRange := strings.Cut(portlist, "-")
var first, last uint64
first, err = strconv.ParseUint(firstStr, 10, 16)
if err != nil {
return "", ports, vizerror.Errorf("port range %#v: invalid first integer", portlist)
}
if isRange {
last, err = strconv.ParseUint(lastStr, 10, 16)
if err != nil {
return "", ports, vizerror.Errorf("port range %#v: invalid last integer", portlist)
}
} else {
last = first
}
if first == 0 {
return "", ports, vizerror.Errorf("port range %#v: first port must be >0, or use '*' for wildcard", portlist)
}
if first > last {
return "", ports, vizerror.Errorf("port range %#v: first port must be >= last port", portlist)
}
return host, newPortRange(uint16(first), uint16(last)), nil
}
func newPortRange(first, last uint16) PortRange {
return PortRange{First: first, Last: last}
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailcfg
import (
"encoding"
"testing"
"tailscale.com/types/ipproto"
"tailscale.com/util/vizerror"
)
var _ encoding.TextUnmarshaler = (*ProtoPortRange)(nil)
func TestProtoPortRangeParsing(t *testing.T) {
pr := func(s, e uint16) PortRange {
return PortRange{First: s, Last: e}
}
tests := []struct {
in string
out ProtoPortRange
err error
}{
{in: "tcp:80", out: ProtoPortRange{Proto: int(ipproto.TCP), Ports: pr(80, 80)}},
{in: "80", out: ProtoPortRange{Ports: pr(80, 80)}},
{in: "*", out: ProtoPortRange{Ports: PortRangeAny}},
{in: "*:*", out: ProtoPortRange{Ports: PortRangeAny}},
{in: "tcp:*", out: ProtoPortRange{Proto: int(ipproto.TCP), Ports: PortRangeAny}},
{
in: "tcp:",
err: vizerror.Errorf("invalid port list: %#v", ""),
},
{
in: ":80",
err: errEmptyProtocol,
},
{
in: "",
err: errEmptyString,
},
}
for _, tc := range tests {
t.Run(tc.in, func(t *testing.T) {
var ppr ProtoPortRange
err := ppr.UnmarshalText([]byte(tc.in))
if tc.err != err {
if err == nil || tc.err.Error() != err.Error() {
t.Fatalf("want err=%v, got %v", tc.err, err)
}
}
if ppr != tc.out {
t.Fatalf("got %v; want %v", ppr, tc.out)
}
})
}
}
func TestProtoPortRangeString(t *testing.T) {
tests := []struct {
input ProtoPortRange
want string
}{
{ProtoPortRange{}, "0"},
// Zero protocol.
{ProtoPortRange{Ports: PortRangeAny}, "*"},
{ProtoPortRange{Ports: PortRange{23, 23}}, "23"},
{ProtoPortRange{Ports: PortRange{80, 120}}, "80-120"},
// Non-zero unnamed protocol.
{ProtoPortRange{Proto: 100, Ports: PortRange{80, 80}}, "100:80"},
{ProtoPortRange{Proto: 200, Ports: PortRange{101, 105}}, "200:101-105"},
// Non-zero named protocol.
{ProtoPortRange{Proto: 1, Ports: PortRangeAny}, "icmp:*"},
{ProtoPortRange{Proto: 2, Ports: PortRangeAny}, "igmp:*"},
{ProtoPortRange{Proto: 6, Ports: PortRange{10, 13}}, "tcp:10-13"},
{ProtoPortRange{Proto: 17, Ports: PortRangeAny}, "udp:*"},
{ProtoPortRange{Proto: 0x84, Ports: PortRange{999, 999}}, "sctp:999"},
{ProtoPortRange{Proto: 0x3a, Ports: PortRangeAny}, "ipv6-icmp:*"},
{ProtoPortRange{Proto: 0x21, Ports: PortRangeAny}, "dccp:*"},
{ProtoPortRange{Proto: 0x2f, Ports: PortRangeAny}, "gre:*"},
}
for _, tc := range tests {
if got := tc.input.String(); got != tc.want {
t.Errorf("String for %v: got %q, want %q", tc.input, got, tc.want)
}
}
}
func TestProtoPortRangeRoundTrip(t *testing.T) {
tests := []struct {
input ProtoPortRange
text string
}{
{ProtoPortRange{Ports: PortRangeAny}, "*"},
{ProtoPortRange{Ports: PortRange{23, 23}}, "23"},
{ProtoPortRange{Ports: PortRange{80, 120}}, "80-120"},
{ProtoPortRange{Proto: 100, Ports: PortRange{80, 80}}, "100:80"},
{ProtoPortRange{Proto: 200, Ports: PortRange{101, 105}}, "200:101-105"},
{ProtoPortRange{Proto: 1, Ports: PortRangeAny}, "icmp:*"},
{ProtoPortRange{Proto: 2, Ports: PortRangeAny}, "igmp:*"},
{ProtoPortRange{Proto: 6, Ports: PortRange{10, 13}}, "tcp:10-13"},
{ProtoPortRange{Proto: 17, Ports: PortRangeAny}, "udp:*"},
{ProtoPortRange{Proto: 0x84, Ports: PortRange{999, 999}}, "sctp:999"},
{ProtoPortRange{Proto: 0x3a, Ports: PortRangeAny}, "ipv6-icmp:*"},
{ProtoPortRange{Proto: 0x21, Ports: PortRangeAny}, "dccp:*"},
{ProtoPortRange{Proto: 0x2f, Ports: PortRangeAny}, "gre:*"},
}
for _, tc := range tests {
out, err := tc.input.MarshalText()
if err != nil {
t.Errorf("MarshalText for %v: %v", tc.input, err)
continue
}
if got := string(out); got != tc.text {
t.Errorf("MarshalText for %#v: got %q, want %q", tc.input, got, tc.text)
}
var ppr ProtoPortRange
if err := ppr.UnmarshalText(out); err != nil {
t.Errorf("UnmarshalText for %q: err=%v", tc.text, err)
continue
}
if ppr != tc.input {
t.Errorf("round trip error for %q: got %v, want %#v", tc.text, ppr, tc.input)
}
}
}

View File

@@ -1100,6 +1100,17 @@ type RegisterRequest struct {
Timestamp *time.Time `json:",omitempty"` // creation time of request to prevent replay
DeviceCert []byte `json:",omitempty"` // X.509 certificate for client device
Signature []byte `json:",omitempty"` // as described by SignatureType
// Tailnet is an optional identifier specifying the name of the recommended or required
// network that the node should join. Its exact form should not be depended on; new
// forms are coming later. The identifier is generally a domain name (for an organization)
// or e-mail address (for a personal account on a shared e-mail provider). It is the same name
// used by the API, as described in /api.md#tailnet.
// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different
// network than the one specified. Otherwise, the server should recommend the specified network
// but still permit logging in to other networks.
// If empty, no recommendation is offered to the server and the login page should show all options.
Tailnet string `json:",omitempty"`
}
// RegisterResponse is returned by the server in response to a RegisterRequest.
@@ -2436,6 +2447,22 @@ type QueryFeatureResponse struct {
ShouldWait bool `json:",omitempty"`
}
// WebClientAuthResponse is the response to a web client authentication request
// sent to "/machine/webclient/action" or "/machine/webclient/wait".
// See client/web for usage.
type WebClientAuthResponse struct {
// Message, if non-empty, provides a message for the user.
Message string `json:",omitempty"`
// Complete is true when the session authentication has been completed.
Complete bool `json:",omitempty"`
// URL is the link for the user to visit to authenticate the session.
//
// When empty, there is no action for the user to take.
URL string `json:",omitempty"`
}
// OverTLSPublicKeyResponse is the JSON response to /key?v=<n>
// over HTTPS (regular TLS) to the Tailscale control plane server,
// where the 'v' argument is the client's current capability version

View File

@@ -362,6 +362,7 @@ var _RegisterRequestCloneNeedsRegeneration = RegisterRequest(struct {
Timestamp *time.Time
DeviceCert []byte
Signature []byte
Tailnet string
}{})
// Clone makes a deep copy of DERPHomeParams.

View File

@@ -792,6 +792,7 @@ func (v RegisterRequestView) DeviceCert() views.ByteSlice[[]byte] {
func (v RegisterRequestView) Signature() views.ByteSlice[[]byte] {
return views.ByteSliceOf(v.ж.Signature)
}
func (v RegisterRequestView) Tailnet() string { return v.ж.Tailnet }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
@@ -810,6 +811,7 @@ var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
Timestamp *time.Time
DeviceCert []byte
Signature []byte
Tailnet string
}{})
// View returns a readonly view of DERPHomeParams.

214
taildrop/resume.go Normal file
View File

@@ -0,0 +1,214 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"slices"
"strings"
)
var (
blockSize = int64(64 << 10)
hashAlgorithm = "sha256"
)
// FileChecksums represents checksums into partially received file.
type FileChecksums struct {
// Offset is the offset into the file.
Offset int64 `json:"offset"`
// Length is the length of content being hashed in the file.
Length int64 `json:"length"`
// Checksums is a list of checksums of BlockSize-sized blocks
// starting from Offset. The number of checksums is the Length
// divided by BlockSize rounded up to the nearest integer.
// All blocks except for the last one are guaranteed to be checksums
// over BlockSize-sized blocks.
Checksums []Checksum `json:"checksums"`
// Algorithm is the hashing algorithm used to compute checksums.
Algorithm string `json:"algorithm"` // always "sha256" for now
// BlockSize is the size of each block.
// The last block may be smaller than this, but never zero.
BlockSize int64 `json:"blockSize"` // always (64<<10) for now
}
// Checksum is an opaque checksum that is comparable.
type Checksum struct{ cs [sha256.Size]byte }
func hash(b []byte) Checksum {
return Checksum{sha256.Sum256(b)}
}
func (cs Checksum) String() string {
return hex.EncodeToString(cs.cs[:])
}
func (cs Checksum) AppendText(b []byte) ([]byte, error) {
return hexAppendEncode(b, cs.cs[:]), nil
}
func (cs Checksum) MarshalText() ([]byte, error) {
return hexAppendEncode(nil, cs.cs[:]), nil
}
func (cs *Checksum) UnmarshalText(b []byte) error {
if len(b) != 2*len(cs.cs) {
return fmt.Errorf("invalid hex length: %d", len(b))
}
_, err := hex.Decode(cs.cs[:], b)
return err
}
// TODO(https://go.dev/issue/53693): Use hex.AppendEncode instead.
func hexAppendEncode(dst, src []byte) []byte {
n := hex.EncodedLen(len(src))
dst = slices.Grow(dst, n)
hex.Encode(dst[len(dst):][:n], src)
return dst[:len(dst)+n]
}
// PartialFiles returns a list of partial files in [Handler.Dir]
// that were sent (or is actively being sent) by the provided id.
func (m *Manager) PartialFiles(id ClientID) (ret []string, err error) {
if m == nil || m.Dir == "" {
return nil, ErrNoTaildrop
}
if m.DirectFileMode && m.AvoidFinalRename {
return nil, nil // resuming is not supported for users that peek at our file structure
}
f, err := os.Open(m.Dir)
if err != nil {
return ret, err
}
defer f.Close()
suffix := id.partialSuffix()
for {
des, err := f.ReadDir(10)
if err != nil {
return ret, err
}
for _, de := range des {
if name := de.Name(); strings.HasSuffix(name, suffix) {
ret = append(ret, name)
}
}
if err == io.EOF {
return ret, nil
}
}
}
// HashPartialFile hashes the contents of a partial file sent by id,
// starting at the specified offset and for the specified length.
// If length is negative, it hashes the entire file.
// If the length exceeds the remaining file length, then it hashes until EOF.
// If [FileHashes.Length] is less than length and no error occurred,
// then it implies that all remaining content in the file has been hashed.
func (m *Manager) HashPartialFile(id ClientID, baseName string, offset, length int64) (FileChecksums, error) {
if m == nil || m.Dir == "" {
return FileChecksums{}, ErrNoTaildrop
}
if m.DirectFileMode && m.AvoidFinalRename {
return FileChecksums{}, nil // resuming is not supported for users that peek at our file structure
}
dstFile, err := m.joinDir(baseName)
if err != nil {
return FileChecksums{}, err
}
f, err := os.Open(dstFile + id.partialSuffix())
if err != nil {
if os.IsNotExist(err) {
return FileChecksums{}, nil
}
return FileChecksums{}, err
}
defer f.Close()
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return FileChecksums{}, err
}
checksums := FileChecksums{
Offset: offset,
Algorithm: hashAlgorithm,
BlockSize: blockSize,
}
b := make([]byte, blockSize) // TODO: Pool this?
r := io.Reader(f)
if length >= 0 {
r = io.LimitReader(f, length)
}
for {
switch n, err := io.ReadFull(r, b); {
case err != nil && err != io.EOF && err != io.ErrUnexpectedEOF:
return checksums, err
case n == 0:
return checksums, nil
default:
checksums.Checksums = append(checksums.Checksums, hash(b[:n]))
checksums.Length += int64(n)
}
}
}
// ResumeReader reads and discards the leading content of r
// that matches the content based on the checksums that exist.
// It returns the number of bytes consumed,
// and returns an [io.Reader] representing the remaining content.
func ResumeReader(r io.Reader, hashFile func(offset, length int64) (FileChecksums, error)) (int64, io.Reader, error) {
if hashFile == nil {
return 0, r, nil
}
// Ask for checksums of a particular content length,
// where the amount of memory needed to represent the checksums themselves
// is exactly equal to the blockSize.
numBlocks := blockSize / sha256.Size
hashLength := blockSize * numBlocks
var offset int64
b := make([]byte, 0, blockSize)
for {
// Request a list of checksums for the partial file starting at offset.
checksums, err := hashFile(offset, hashLength)
if len(checksums.Checksums) == 0 || err != nil {
return offset, io.MultiReader(bytes.NewReader(b), r), err
} else if checksums.BlockSize != blockSize || checksums.Algorithm != hashAlgorithm {
return offset, io.MultiReader(bytes.NewReader(b), r), fmt.Errorf("invalid block size or hashing algorithm")
}
// Read from r, comparing each block with the provided checksums.
for _, want := range checksums.Checksums {
// Read a block from r.
n, err := io.ReadFull(r, b[:blockSize])
b = b[:n]
if err == io.EOF || err == io.ErrUnexpectedEOF {
err = nil
}
if len(b) == 0 || err != nil {
// This should not occur in practice.
// It implies that an error occurred reading r,
// or that the partial file on the remote side is fully complete.
return offset, io.MultiReader(bytes.NewReader(b), r), err
}
// Compare the local and remote block checksums.
// If it mismatches, then resume from this point.
got := hash(b)
if got != want {
return offset, io.MultiReader(bytes.NewReader(b), r), nil
}
offset += int64(len(b))
b = b[:0]
}
// We hashed the remainder of the partial file, so stop.
if checksums.Length < hashLength {
return offset, io.MultiReader(bytes.NewReader(b), r), nil
}
}
}

63
taildrop/resume_test.go Normal file
View File

@@ -0,0 +1,63 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"bytes"
"io"
"math/rand"
"os"
"testing"
"testing/iotest"
"tailscale.com/util/must"
)
func TestResume(t *testing.T) {
oldBlockSize := blockSize
defer func() { blockSize = oldBlockSize }()
blockSize = 256
m := Manager{Logf: t.Logf, Dir: t.TempDir()}
rn := rand.New(rand.NewSource(0))
want := make([]byte, 12345)
must.Get(io.ReadFull(rn, want))
t.Run("resume-noop", func(t *testing.T) {
r := io.Reader(bytes.NewReader(want))
offset, r, err := ResumeReader(r, func(offset, length int64) (FileChecksums, error) {
return m.HashPartialFile("", "foo", offset, length)
})
must.Do(err)
must.Get(m.PutFile("", "foo", r, offset, -1))
got := must.Get(os.ReadFile(must.Get(m.joinDir("foo"))))
if !bytes.Equal(got, want) {
t.Errorf("content mismatches")
}
})
t.Run("resume-retry", func(t *testing.T) {
rn := rand.New(rand.NewSource(0))
for {
r := io.Reader(bytes.NewReader(want))
offset, r, err := ResumeReader(r, func(offset, length int64) (FileChecksums, error) {
return m.HashPartialFile("", "foo", offset, length)
})
must.Do(err)
numWant := rn.Int63n(min(int64(len(want))-offset, 1000) + 1)
if offset < int64(len(want)) {
r = io.MultiReader(io.LimitReader(r, numWant), iotest.ErrReader(io.ErrClosedPipe))
}
if _, err := m.PutFile("", "foo", r, offset, -1); err == nil {
break
}
}
got := must.Get(os.ReadFile(must.Get(m.joinDir("foo"))))
if !bytes.Equal(got, want) {
t.Errorf("content mismatches")
}
})
}

View File

@@ -19,13 +19,13 @@ import (
"tailscale.com/logtail/backoff"
)
// HasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *Handler) HasFilesWaiting() bool {
if s == nil || s.RootDir == "" || s.DirectFileMode {
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
// This always returns false when [Handler.DirectFileMode] is false.
func (m *Manager) HasFilesWaiting() bool {
if m == nil || m.Dir == "" || m.DirectFileMode {
return false
}
if s.KnownEmpty.Load() {
if m.knownEmpty.Load() {
// Optimization: this is usually empty, so avoid opening
// the directory and checking. We can't cache the actual
// has-files-or-not values as the macOS/iOS client might
@@ -33,7 +33,7 @@ func (s *Handler) HasFilesWaiting() bool {
// keep this negative cache.
return false
}
f, err := os.Open(s.RootDir)
f, err := os.Open(m.Dir)
if err != nil {
return false
}
@@ -42,7 +42,7 @@ func (s *Handler) HasFilesWaiting() bool {
des, err := f.ReadDir(10)
for _, de := range des {
name := de.Name()
if strings.HasSuffix(name, PartialSuffix) {
if strings.HasSuffix(name, partialSuffix) {
continue
}
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@@ -51,22 +51,22 @@ func (s *Handler) HasFilesWaiting() bool {
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
// and we don't want to delete the ".deleted" file before
// enumerating to the "foo.jpg" file.
defer tryDeleteAgain(filepath.Join(s.RootDir, name))
defer tryDeleteAgain(filepath.Join(m.Dir, name))
continue
}
if de.Type().IsRegular() {
_, err := os.Stat(filepath.Join(s.RootDir, name+deletedSuffix))
_, err := os.Stat(filepath.Join(m.Dir, name+deletedSuffix))
if os.IsNotExist(err) {
return true
}
if err == nil {
tryDeleteAgain(filepath.Join(s.RootDir, name))
tryDeleteAgain(filepath.Join(m.Dir, name))
continue
}
}
}
if err == io.EOF {
s.KnownEmpty.Store(true)
m.knownEmpty.Store(true)
}
if err != nil {
break
@@ -76,22 +76,16 @@ func (s *Handler) HasFilesWaiting() bool {
}
// WaitingFiles returns the list of files that have been sent by a
// peer that are waiting in the buffered "pick up" directory owned by
// the Tailscale daemon.
//
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s == nil {
return nil, errNilHandler
}
if s.RootDir == "" {
// peer that are waiting in [Handler.Dir].
// This always returns nil when [Handler.DirectFileMode] is false.
func (m *Manager) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if m == nil || m.Dir == "" {
return nil, ErrNoTaildrop
}
if s.DirectFileMode {
if m.DirectFileMode {
return nil, nil
}
f, err := os.Open(s.RootDir)
f, err := os.Open(m.Dir)
if err != nil {
return nil, err
}
@@ -101,7 +95,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
des, err := f.ReadDir(10)
for _, de := range des {
name := de.Name()
if strings.HasSuffix(name, PartialSuffix) {
if strings.HasSuffix(name, partialSuffix) {
continue
}
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@@ -143,7 +137,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
// Maybe Windows is done virus scanning the file we tried
// to delete a long time ago and will let us delete it now.
for name := range deleted {
tryDeleteAgain(filepath.Join(s.RootDir, name))
tryDeleteAgain(filepath.Join(m.Dir, name))
}
}
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
@@ -164,27 +158,26 @@ func tryDeleteAgain(fullPath string) {
}
}
func (s *Handler) DeleteFile(baseName string) error {
if s == nil {
return errNilHandler
}
if s.RootDir == "" {
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (m *Manager) DeleteFile(baseName string) error {
if m == nil || m.Dir == "" {
return ErrNoTaildrop
}
if s.DirectFileMode {
if m.DirectFileMode {
return errors.New("deletes not allowed in direct mode")
}
path, ok := s.DiskPath(baseName)
if !ok {
return errors.New("bad filename")
path, err := m.joinDir(baseName)
if err != nil {
return err
}
var bo *backoff.Backoff
logf := s.Logf
t0 := s.Clock.Now()
logf := m.Logf
t0 := m.Clock.Now()
for {
err := os.Remove(path)
if err != nil && !os.IsNotExist(err) {
err = RedactErr(err)
err = redactErr(err)
// Put a retry loop around deletes on Windows. Windows
// file descriptor closes are effectively asynchronous,
// as a bunch of hooks run on/after close, and we can't
@@ -199,11 +192,11 @@ func (s *Handler) DeleteFile(baseName string) error {
if bo == nil {
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
}
if s.Clock.Since(t0) < 5*time.Second {
if m.Clock.Since(t0) < 5*time.Second {
bo.BackOff(context.Background(), err)
continue
}
if err := TouchFile(path + deletedSuffix); err != nil {
if err := touchFile(path + deletedSuffix); err != nil {
logf("peerapi: failed to leave deleted marker: %v", err)
}
}
@@ -214,27 +207,26 @@ func (s *Handler) DeleteFile(baseName string) error {
}
}
func TouchFile(path string) error {
func touchFile(path string) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return RedactErr(err)
return redactErr(err)
}
return f.Close()
}
func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s == nil {
return nil, 0, errNilHandler
}
if s.RootDir == "" {
// OpenFile opens a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (m *Manager) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if m == nil || m.Dir == "" {
return nil, 0, ErrNoTaildrop
}
if s.DirectFileMode {
if m.DirectFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
}
path, ok := s.DiskPath(baseName)
if !ok {
return nil, 0, errors.New("bad filename")
path, err := m.joinDir(baseName)
if err != nil {
return nil, 0, err
}
if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
tryDeleteAgain(path)
@@ -242,12 +234,12 @@ func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err e
}
f, err := os.Open(path)
if err != nil {
return nil, 0, RedactErr(err)
return nil, 0, redactErr(err)
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, RedactErr(err)
return nil, 0, redactErr(err)
}
return f, fi.Size(), nil
}

255
taildrop/send.go Normal file
View File

@@ -0,0 +1,255 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"crypto/sha256"
"errors"
"io"
"os"
"sync"
"time"
"tailscale.com/envknob"
"tailscale.com/tstime"
"tailscale.com/version/distro"
)
type incomingFileKey struct {
id ClientID
name string // e.g., "foo.jpeg"
}
type incomingFile struct {
clock tstime.DefaultClock
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
sendFileNotify func() // called when done
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
var needNotify bool
defer func() {
if needNotify {
f.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := f.clock.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
// PutFile stores a file into [Manager.Dir] from a given client id.
// The baseName must be a base filename without any slashes.
// The length is the expected length of content to read from r,
// it may be negative to indicate that it is unknown.
// It returns the length of the entire file.
//
// If there is a failure reading from r, then the partial file is not deleted
// for some period of time. The [Manager.PartialFiles] and [Manager.HashPartialFile]
// methods may be used to list all partial files and to compute the hash for a
// specific partial file. This allows the client to determine whether to resume
// a partial file. While resuming, PutFile may be called again with a non-zero
// offset to specify where to resume receiving data at.
func (m *Manager) PutFile(id ClientID, baseName string, r io.Reader, offset, length int64) (int64, error) {
switch {
case m == nil || m.Dir == "":
return 0, ErrNoTaildrop
case !envknob.CanTaildrop():
return 0, ErrNoTaildrop
case distro.Get() == distro.Unraid && !m.DirectFileMode:
return 0, ErrNotAccessible
}
dstPath, err := m.joinDir(baseName)
if err != nil {
return 0, err
}
redactAndLogError := func(action string, err error) error {
err = redactErr(err)
m.Logf("put %v error: %v", action, err)
return err
}
avoidPartialRename := m.DirectFileMode && m.AvoidFinalRename
if avoidPartialRename {
// Users using AvoidFinalRename are depending on the exact filename
// of the partial files. So avoid injecting the id into it.
id = ""
}
// Check whether there is an in-progress transfer for the file.
sendFileNotify := m.SendFileNotify
if sendFileNotify == nil {
sendFileNotify = func() {} // avoid nil panics below
}
partialPath := dstPath + id.partialSuffix()
inFileKey := incomingFileKey{id, baseName}
inFile, loaded := m.incomingFiles.LoadOrInit(inFileKey, func() *incomingFile {
inFile := &incomingFile{
clock: m.Clock,
started: m.Clock.Now(),
size: length,
sendFileNotify: sendFileNotify,
}
if m.DirectFileMode {
inFile.partialPath = partialPath
}
return inFile
})
if loaded {
return 0, ErrFileExists
}
defer m.incomingFiles.Delete(inFileKey)
// Create (if not already) the partial file with read-write permissions.
f, err := os.OpenFile(partialPath, os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
return 0, redactAndLogError("Create", err)
}
defer func() {
f.Close() // best-effort to cleanup dangling file handles
if err != nil {
if avoidPartialRename {
os.Remove(partialPath) // best-effort
return
}
// TODO: We need to delete partialPath eventually.
// However, this must be done after some period of time.
}
}()
inFile.w = f
// A positive offset implies that we are resuming an existing file.
// Seek to the appropriate offset and truncate the file.
if offset != 0 {
currLength, err := f.Seek(0, io.SeekEnd)
if err != nil {
return 0, redactAndLogError("Seek", err)
}
if offset < 0 || offset > currLength {
return 0, redactAndLogError("Seek", err)
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return 0, redactAndLogError("Seek", err)
}
if err := f.Truncate(offset); err != nil {
return 0, redactAndLogError("Truncate", err)
}
}
// Copy the contents of the file.
copyLength, err := io.Copy(inFile, r)
if err != nil {
return 0, redactAndLogError("Copy", err)
}
if length >= 0 && copyLength != length {
return 0, redactAndLogError("Copy", errors.New("copied an unexpected number of bytes"))
}
if err := f.Close(); err != nil {
return 0, redactAndLogError("Close", err)
}
fileLength := offset + copyLength
// Return early for avoidPartialRename since users of AvoidFinalRename
// are depending on the exact naming of partial files.
if avoidPartialRename {
inFile.mu.Lock()
inFile.done = true
inFile.mu.Unlock()
m.knownEmpty.Store(false)
sendFileNotify()
return fileLength, nil
}
// File has been successfully received, rename the partial file
// to the final destination filename. If a file of that name already exists,
// then try multiple times with variations of the filename.
computePartialSum := sync.OnceValues(func() ([sha256.Size]byte, error) {
return sha256File(partialPath)
})
maxRetries := 10
for ; maxRetries > 0; maxRetries-- {
// Atomically rename the partial file as the destination file if it doesn't exist.
// Otherwise, it returns the length of the current destination file.
// The operation is atomic.
dstLength, err := func() (int64, error) {
m.renameMu.Lock()
defer m.renameMu.Unlock()
switch fi, err := os.Stat(dstPath); {
case os.IsNotExist(err):
return -1, os.Rename(partialPath, dstPath)
case err != nil:
return -1, err
default:
return fi.Size(), nil
}
}()
if err != nil {
return 0, redactAndLogError("Rename", err)
}
if dstLength < 0 {
break // we successfully renamed; so stop
}
// Avoid the final rename if a destination file has the same contents.
if dstLength == fileLength {
partialSum, err := computePartialSum()
if err != nil {
return 0, redactAndLogError("Rename", err)
}
dstSum, err := sha256File(dstPath)
if err != nil {
return 0, redactAndLogError("Rename", err)
}
if dstSum == partialSum {
if err := os.Remove(partialPath); err != nil {
return 0, redactAndLogError("Remove", err)
}
break // we successfully found a content match; so stop
}
}
// Choose a new destination filename and try again.
dstPath = NextFilename(dstPath)
}
if maxRetries <= 0 {
return 0, errors.New("too many retries trying to rename partial file")
}
m.knownEmpty.Store(false)
sendFileNotify()
return fileLength, nil
}
func sha256File(file string) (out [sha256.Size]byte, err error) {
h := sha256.New()
f, err := os.Open(file)
if err != nil {
return out, err
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return out, err
}
return [sha256.Size]byte(h.Sum(nil)), nil
}

View File

@@ -1,6 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package taildrop contains the implementation of the Taildrop
// functionality including sending and retrieving files.
// This package does not validate permissions, the caller should
// be responsible for ensuring correct authorization.
//
// For related documentation see: http://go/taildrop-how-does-it-work
package taildrop
import (
@@ -9,50 +15,91 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"unicode"
"unicode/utf8"
"tailscale.com/ipn"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
type Handler struct {
// ClientID is an opaque identifier for file resumption.
// A client can only list and resume partial files for its own ID.
// It must contain any filesystem specific characters (e.g., slashes).
type ClientID string // e.g., "n12345CNTRL"
func (id ClientID) partialSuffix() string {
if id == "" {
return partialSuffix
}
return "." + string(id) + partialSuffix // e.g., ".n12345CNTRL.partial"
}
// Manager manages the state for receiving and managing taildropped files.
type Manager struct {
Logf logger.Logf
Clock tstime.Clock
Clock tstime.DefaultClock
RootDir string // empty means file receiving unavailable
// Dir is the directory to store received files.
// This main either be the final location for the files
// or just a temporary staging directory (see DirectFileMode).
Dir string
// DirectFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on the GUI macOS versions
// and on Synology.
// In DirectFileMode, the peerapi doesn't do the final rename
// from "foo.jpg.partial" to "foo.jpg" unless
// directFileDoFinalRename is set.
// DirectFileMode reports whether we are writing files
// directly to a download directory, rather than writing them to
// a temporary staging directory.
//
// The following methods:
// - HasFilesWaiting
// - WaitingFiles
// - DeleteFile
// - OpenFile
// have no purpose in DirectFileMode.
// They are only used to check whether files are in the staging directory,
// copy them out, and then delete them.
DirectFileMode bool
// DirectFileDoFinalRename is whether in directFileMode we
// additionally move the *.direct file to its final name after
// it's received.
DirectFileDoFinalRename bool
// AvoidFinalRename specifies whether in DirectFileMode
// we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
//
// TODO(joetsai,rhea): Delete this. This is currently depended upon
// in the Apple platforms since it violates the abstraction layer
// and directly assumes how taildrop represents partial files.
// Right now, file resumption does not work on Apple.
AvoidFinalRename bool
KnownEmpty atomic.Bool
// SendFileNotify is called periodically while a file is actively
// receiving the contents for the file. There is a final call
// to the function when reception completes.
// It is not called if nil.
SendFileNotify func()
knownEmpty atomic.Bool
incomingFiles syncs.Map[incomingFileKey, *incomingFile]
// renameMu is used to protect os.Rename calls so that they are atomic.
renameMu sync.Mutex
}
var (
errNilHandler = errors.New("handler unavailable; not listening")
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
ErrInvalidFileName = errors.New("invalid filename")
ErrFileExists = errors.New("file already exists")
ErrNotAccessible = errors.New("Taildrop folder not configured or accessible")
)
const (
// PartialSuffix is the suffix appended to files while they're
// partialSuffix is the suffix appended to files while they're
// still in the process of being transferred.
PartialSuffix = ".partial"
partialSuffix = ".partial"
// deletedSuffix is the suffix for a deleted marker file
// that's placed next to a file (without the suffix) that we
@@ -84,33 +131,55 @@ func validFilenameRune(r rune) bool {
return unicode.IsPrint(r)
}
func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
func (m *Manager) joinDir(baseName string) (fullPath string, err error) {
if !utf8.ValidString(baseName) {
return "", false
return "", ErrInvalidFileName
}
if strings.TrimSpace(baseName) != baseName {
return "", false
return "", ErrInvalidFileName
}
if len(baseName) > 255 {
return "", false
return "", ErrInvalidFileName
}
// TODO: validate unicode normalization form too? Varies by platform.
clean := path.Clean(baseName)
if clean != baseName ||
clean == "." || clean == ".." ||
strings.HasSuffix(clean, deletedSuffix) ||
strings.HasSuffix(clean, PartialSuffix) {
return "", false
strings.HasSuffix(clean, partialSuffix) {
return "", ErrInvalidFileName
}
for _, r := range baseName {
if !validFilenameRune(r) {
return "", false
return "", ErrInvalidFileName
}
}
if !filepath.IsLocal(baseName) {
return "", false
return "", ErrInvalidFileName
}
return filepath.Join(s.RootDir, baseName), true
return filepath.Join(m.Dir, baseName), nil
}
// IncomingFiles returns a list of active incoming files.
func (m *Manager) IncomingFiles() []ipn.PartialFile {
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
// in JSON to clients. They distinguish between empty and non-nil
// to know whether a Notify should be able about files.
files := make([]ipn.PartialFile, 0)
m.incomingFiles.Range(func(k incomingFileKey, f *incomingFile) bool {
f.mu.Lock()
defer f.mu.Unlock()
files = append(files, ipn.PartialFile{
Name: k.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
})
return true
})
return files
}
type redactedErr struct {
@@ -136,7 +205,7 @@ func redactString(s string) string {
return string(b)
}
func RedactErr(root error) error {
func redactErr(root error) error {
// redactStrings is a list of sensitive strings that were redacted.
// It is not sufficient to just snub out sensitive fields in Go errors
// since some wrapper errors like fmt.Errorf pre-cache the error string,
@@ -176,3 +245,26 @@ func RedactErr(root error) error {
}
return &redactedErr{msg: s, inner: root}
}
var (
rxExtensionSuffix = regexp.MustCompile(`(\.[a-zA-Z0-9]{0,3}[a-zA-Z][a-zA-Z0-9]{0,3})*$`)
rxNumberSuffix = regexp.MustCompile(` \([0-9]+\)`)
)
// NextFilename returns the next filename in a sequence.
// It is used for construction a new filename if there is a conflict.
//
// For example, "Foo.jpg" becomes "Foo (1).jpg" and
// "Foo (1).jpg" becomes "Foo (2).jpg".
func NextFilename(name string) string {
ext := rxExtensionSuffix.FindString(strings.TrimPrefix(name, "."))
name = strings.TrimSuffix(name, ext)
var n uint64
if rxNumberSuffix.MatchString(name) {
i := strings.LastIndex(name, " (")
if n, _ = strconv.ParseUint(name[i+len("( "):len(name)-len(")")], 10, 64); n > 0 {
name = name[:i]
}
}
return name + " (" + strconv.FormatUint(n+1, 10) + ")" + ext
}

184
taildrop/taildrop_test.go Normal file
View File

@@ -0,0 +1,184 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"testing"
)
// Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir()
h := &Manager{Dir: dir}
nothingWaiting := func() {
t.Helper()
h.knownEmpty.Store(false)
if h.HasFilesWaiting() {
t.Fatal("unexpected files waiting")
}
}
touch := func(base string) {
t.Helper()
if err := touchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err)
}
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {
t.Errorf("unexpected file in tempdir: %q", fi.Name())
}
}
}
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
wf, err := h.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wf) != 0 {
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
}
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
if rc, _, err := h.OpenFile("foo.jpg"); err == nil {
rc.Close()
t.Fatal("unexpected foo.jpg open")
}
wantEmptyTempDir()
// And verify basics still work in non-deleted cases.
touch("foo.jpg")
touch("bar.jpg.deleted")
if wf, err := h.WaitingFiles(); err != nil {
t.Error(err)
} else if len(wf) != 1 {
t.Errorf("WaitingFiles = %d; want 1", len(wf))
} else if wf[0].Name != "foo.jpg" {
t.Errorf("unexpected waiting file %+v", wf[0])
}
if rc, _, err := h.OpenFile("foo.jpg"); err != nil {
t.Fatal(err)
} else {
rc.Close()
}
}
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := redactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := redactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}
func TestNextFilename(t *testing.T) {
tests := []struct {
in string
want string
want2 string
}{
{"foo", "foo (1)", "foo (2)"},
{"foo(1)", "foo(1) (1)", "foo(1) (2)"},
{"foo.tar", "foo (1).tar", "foo (2).tar"},
{"foo.tar.gz", "foo (1).tar.gz", "foo (2).tar.gz"},
{".bashrc", ".bashrc (1)", ".bashrc (2)"},
{"fizz buzz.torrent", "fizz buzz (1).torrent", "fizz buzz (2).torrent"},
{"rawr 2023.12.15.txt", "rawr 2023.12.15 (1).txt", "rawr 2023.12.15 (2).txt"},
{"IMG_7934.JPEG", "IMG_7934 (1).JPEG", "IMG_7934 (2).JPEG"},
{"my song.mp3", "my song (1).mp3", "my song (2).mp3"},
{"archive.7z", "archive (1).7z", "archive (2).7z"},
{"foo/bar/fizz", "foo/bar/fizz (1)", "foo/bar/fizz (2)"},
}
for _, tt := range tests {
if got := NextFilename(tt.in); got != tt.want {
t.Errorf("NextFilename(%q) = %q, want %q", tt.in, got, tt.want)
}
if got2 := NextFilename(tt.want); got2 != tt.want2 {
t.Errorf("NextFilename(%q) = %q, want %q", tt.want, got2, tt.want2)
}
}
}

View File

@@ -32,7 +32,10 @@ if [[ -d "$toolchain" ]]; then
# A toolchain exists, but is it recent enough to compile gocross? If not,
# wipe it out so that the next if block fetches a usable one.
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
have_go_minor=""
if [[ -f "$toolchain/VERSION" ]]; then
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
fi
# Shortly before stable releases, we run release candidate
# toolchains, which have a non-numeric suffix on the version
# number. Remove the rc qualifier, we just care about the minor

View File

@@ -23,6 +23,7 @@ import (
"tailscale.com/control/controlknobs"
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
@@ -47,6 +48,16 @@ type System struct {
StateStore SubSystem[ipn.StateStore]
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
// InitialConfig is initial server config, if any.
// It is nil if the node is not in declarative mode.
// This value is never updated after startup.
// LocalBackend tracks the current config after any reloads.
InitialConfig *conffile.Config
// onlyNetstack is whether the Tun value is a fake TUN device
// and we're using netstack for everything.
onlyNetstack bool
controlKnobs controlknobs.Knobs
proxyMap proxymap.Mapper
}
@@ -74,6 +85,12 @@ func (s *System) Set(v any) {
case router.Router:
s.Router.Set(v)
case *tstun.Wrapper:
type ft interface {
IsFakeTun() bool
}
if _, ok := v.Unwrap().(ft); ok {
s.onlyNetstack = true
}
s.Tun.Set(v)
case *magicsock.Conn:
s.MagicSock.Set(v)
@@ -97,8 +114,7 @@ func (s *System) IsNetstackRouter() bool {
// IsNetstack reports whether Tailscale is running as a netstack-based TUN-free engine.
func (s *System) IsNetstack() bool {
name, _ := s.Tun.Get().Name()
return name == tstun.FakeTUNName
return s.onlyNetstack
}
// ControlKnobs returns the control knobs for this node.

View File

@@ -5,7 +5,6 @@
package main
import (
"context"
"flag"
"log"
"net/http"
@@ -21,7 +20,6 @@ var (
func main() {
flag.Parse()
ctx := context.Background()
s := new(tsnet.Server)
defer s.Close()
@@ -32,7 +30,7 @@ func main() {
}
// Serve the Tailscale web client.
ws, cleanup := web.NewServer(ctx, web.ServerOpts{
ws, cleanup := web.NewServer(web.ServerOpts{
DevMode: *devMode,
LocalClient: lc,
})

View File

@@ -53,6 +53,7 @@ import (
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/testenv"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
@@ -133,12 +134,26 @@ type Server struct {
logtail *logtail.Logger
logid logid.PublicID
mu sync.Mutex
listeners map[listenKey]*listener
dialer *tsdial.Dialer
closed bool
mu sync.Mutex
listeners map[listenKey]*listener
fallbackTCPHandlers set.HandleSet[FallbackTCPHandler]
dialer *tsdial.Dialer
closed bool
}
// FallbackTCPHandler describes the callback which
// conditionally handles an incoming TCP flow for the
// provided (src/port, dst/port) 4-tuple. These are registered
// as handlers of last resort, and are called only if no
// listener could handle the incoming flow.
//
// If the callback returns intercept=false, the flow is rejected.
//
// When intercept=true, the behavior depends on whether the returned handler
// is non-nil: if nil, the connection is rejected. If non-nil, handler takes
// over the TCP conn.
type FallbackTCPHandler func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool)
// Dial connects to the address on the tailnet.
// It will start the server if it has not been started yet.
func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) {
@@ -515,6 +530,7 @@ func (s *Server) start() (reterr error) {
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
sys.Tun.Get().Start()
sys.Set(ns)
ns.ProcessLocalIPs = true
ns.GetTCPHandlerForFlow = s.getTCPHandlerForFlow
@@ -755,6 +771,14 @@ func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16)
func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
ln, ok := s.listenerForDstAddr("tcp", dst, false)
if !ok {
s.mu.Lock()
defer s.mu.Unlock()
for _, handler := range s.fallbackTCPHandlers {
connHandler, intercept := handler(src, dst)
if intercept {
return connHandler, intercept
}
}
return nil, true // don't handle, don't forward to localhost
}
return ln.handle, true
@@ -858,6 +882,24 @@ func (s *Server) ListenTLS(network, addr string) (net.Listener, error) {
}), nil
}
// RegisterFallbackTCPHandler registers a callback which will be called
// to handle a TCP flow to this tsnet node, for which no listeners will handle.
//
// If multiple fallback handlers are registered, they will be called in an
// undefined order. See FallbackTCPHandler for details on handling a flow.
//
// The returned function can be used to deregister this callback.
func (s *Server) RegisterFallbackTCPHandler(cb FallbackTCPHandler) func() {
s.mu.Lock()
defer s.mu.Unlock()
hnd := s.fallbackTCPHandlers.Add(cb)
return func() {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.fallbackTCPHandlers, hnd)
}
}
// getCert is the GetCertificate function used by ListenTLS.
//
// It calls GetCertificate on the localClient, passing in the ClientHelloInfo.

View File

@@ -26,6 +26,7 @@ import (
"reflect"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -630,3 +631,45 @@ type bufferedConn struct {
func (c *bufferedConn) Read(b []byte) (int, error) {
return c.reader.Read(b)
}
func TestFallbackTCPHandler(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
controlURL := startControl(t)
s1, s1ip := startServer(t, ctx, controlURL, "s1")
s2, _ := startServer(t, ctx, controlURL, "s2")
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// ping to make sure the connection is up.
res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
var s1TcpConnCount atomic.Int32
deregister := s1.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
s1TcpConnCount.Add(1)
return nil, false
})
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
t.Fatal("Expected dial error because fallback handler did not intercept")
}
if got := s1TcpConnCount.Load(); got != 1 {
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
}
deregister()
if _, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip)); err == nil {
t.Fatal("Expected dial error because nothing would intercept")
}
if got := s1TcpConnCount.Load(); got != 1 {
t.Errorf("s1TcpConnCount = %d, want %d", got, 1)
}
}

View File

@@ -117,7 +117,11 @@ func build(outDir string, targets ...string) error {
// Fallback slow path for cross-compiled binaries.
for _, target := range targets {
outFile := filepath.Join(outDir, path.Base(target)+exe())
cmd := exec.Command(goBin, "build", "-o", outFile, target)
cmd := exec.Command(goBin, "build", "-o", outFile)
if version.IsRace() {
cmd.Args = append(cmd.Args, "-race")
}
cmd.Args = append(cmd.Args, target)
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
if errOut, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
@@ -138,6 +142,16 @@ func findGo() (string, error) {
// 2. Look for a go binary in runtime.GOROOT()/bin if runtime.GOROOT() is non-empty.
// 3. Look for a go binary in $PATH.
// For tests we want to run as root on GitHub actions, we run with -exec=sudo,
// but that results in this test running with a different PATH and picking the
// wrong Go. So hard code the GitHub Actions case.
if os.Getuid() == 0 && os.Getenv("GITHUB_ACTIONS") == "true" {
const sudoGithubGo = "/home/runner/.cache/tailscale-go/bin/go"
if _, err := os.Stat(sudoGithubGo); err == nil {
return sudoGithubGo, nil
}
}
paths := strings.FieldsFunc(os.Getenv("PATH"), func(r rune) bool { return os.IsPathSeparator(uint8(r)) })
if len(paths) > 0 {
candidate := filepath.Join(paths[0], "go"+exe())

View File

@@ -42,6 +42,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/rands"
"tailscale.com/version"
)
var (
@@ -67,8 +68,36 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
// parallel marks t as a parallel test if not root.
// In root mode, the tun devices conflict.
func parallel(t *testing.T) {
if os.Getuid() != 0 {
t.Parallel()
}
}
// Tests that tailscaled starts up in TUN mode, and also without data races:
// https://github.com/tailscale/tailscale/issues/7894
func TestTUNMode(t *testing.T) {
if os.Getuid() != 0 {
t.Skip("skipping when not root; redundant with other tests")
}
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestOneNodeUpNoAuth(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
@@ -85,7 +114,7 @@ func TestOneNodeUpNoAuth(t *testing.T) {
}
func TestOneNodeExpiredKey(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
@@ -121,7 +150,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
}
func TestControlKnobs(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
@@ -151,7 +180,7 @@ func TestControlKnobs(t *testing.T) {
}
func TestCollectPanic(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n := newTestNode(t, env)
@@ -181,7 +210,7 @@ func TestCollectPanic(t *testing.T) {
}
func TestControlTimeLogLine(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
env.LogCatcher.StoreRawJSON()
n := newTestNode(t, env)
@@ -204,7 +233,7 @@ func TestControlTimeLogLine(t *testing.T) {
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
func TestStateSavedOnStart(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
@@ -240,7 +269,7 @@ func TestStateSavedOnStart(t *testing.T) {
}
func TestOneNodeUpAuth(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t, configureControl(func(control *testcontrol.Server) {
control.RequireAuth = true
}))
@@ -284,7 +313,7 @@ func TestOneNodeUpAuth(t *testing.T) {
func TestTwoNodes(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/3598")
t.Parallel()
parallel(t)
env := newTestEnv(t)
// Create two nodes:
@@ -333,7 +362,7 @@ func TestTwoNodes(t *testing.T) {
// PeersRemoved set) saying that the second node disappeared.
func TestIncrementalMapUpdatePeersRemoved(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/3598")
t.Parallel()
parallel(t)
env := newTestEnv(t)
// Create one node:
@@ -417,7 +446,7 @@ func TestIncrementalMapUpdatePeersRemoved(t *testing.T) {
func TestNodeAddressIPFields(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/7008")
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
d1 := n1.StartDaemon()
@@ -443,7 +472,7 @@ func TestNodeAddressIPFields(t *testing.T) {
}
func TestAddPingRequest(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.StartDaemon()
@@ -495,7 +524,7 @@ func TestAddPingRequest(t *testing.T) {
}
func TestC2NPingRequest(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.StartDaemon()
@@ -565,7 +594,7 @@ func TestC2NPingRequest(t *testing.T) {
// Issue 2434: when "down" (WantRunning false), tailscaled shouldn't
// be connected to control.
func TestNoControlConnWhenDown(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
@@ -606,7 +635,7 @@ func TestNoControlConnWhenDown(t *testing.T) {
// Issue 2137: make sure Windows tailscaled works with the CLI alone,
// without the GUI to kick off a Start.
func TestOneNodeUpWindowsStyle(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
n1 := newTestNode(t, env)
n1.upFlagGOOS = "windows"
@@ -624,7 +653,7 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
// TestNATPing creates two nodes, n1 and n2, sets up masquerades for both and
// tries to do bi-directional pings between them.
func TestNATPing(t *testing.T) {
t.Parallel()
parallel(t)
for _, v6 := range []bool{false, true} {
env := newTestEnv(t)
registerNode := func() (*testNode, key.NodePublic) {
@@ -751,7 +780,7 @@ func TestNATPing(t *testing.T) {
}
func TestLogoutRemovesAllPeers(t *testing.T) {
t.Parallel()
parallel(t)
env := newTestEnv(t)
// Spin up some nodes.
nodes := make([]*testNode, 2)
@@ -810,6 +839,7 @@ type testEnv struct {
t testing.TB
cli string
daemon string
didTun atomic.Bool // made a TUN node
LogCatcher *LogCatcher
LogCatcherServer *httptest.Server
@@ -877,7 +907,8 @@ func newTestEnv(t testing.TB, opts ...testEnvOpt) *testEnv {
// Currently, the test is simplistic and user==node==machine.
// That may grow complexity later to test more.
type testNode struct {
env *testEnv
env *testEnv
tunMode bool
dir string // temp dir for sock & state
sockFile string
@@ -898,12 +929,29 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
sockFile = filepath.Join(os.TempDir(), rands.HexString(8)+".sock")
t.Cleanup(func() { os.Remove(sockFile) })
}
return &testNode{
n := &testNode{
env: env,
dir: dir,
sockFile: sockFile,
stateFile: filepath.Join(dir, "tailscale.state"),
// The first node per test gets to be in TUN mode when run
// as root.
tunMode: os.Getuid() == 0 && env.didTun.CompareAndSwap(false, true),
}
// Look for a data race. Once we see the start marker, start logging the rest.
var sawRace bool
n.addLogLineHook(func(line []byte) {
if mem.Contains(mem.B(line), mem.S("WARNING: DATA RACE")) {
sawRace = true
}
if sawRace {
t.Logf("%s", line)
}
})
return n
}
func (n *testNode) diskPrefs() *ipn.Prefs {
@@ -962,7 +1010,7 @@ func (n *testNode) socks5AddrChan() <-chan string {
if i == -1 {
return
}
addr := string(line)[i+len(sub):]
addr := strings.TrimSpace(string(line)[i+len(sub):])
select {
case ch <- addr:
default:
@@ -1009,11 +1057,10 @@ func (op *nodeOutputParser) parseLines() {
}
line := buf[:nl+1]
buf = buf[nl+1:]
lineTrim := bytes.TrimSpace(line)
n.mu.Lock()
for _, f := range n.onLogLine {
f(lineTrim)
f(line)
}
n.mu.Unlock()
}
@@ -1047,8 +1094,8 @@ func (n *testNode) StartDaemon() *Daemon {
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
t := n.env.t
cmd := exec.Command(n.env.daemon,
"--tun=userspace-networking",
cmd := exec.Command(n.env.daemon)
cmd.Args = append(cmd.Args,
"--state="+n.stateFile,
"--socket="+n.sockFile,
"--socks5-server=localhost:0",
@@ -1056,6 +1103,11 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
if *verboseTailscaled {
cmd.Args = append(cmd.Args, "-verbose=2")
}
if !n.tunMode {
cmd.Args = append(cmd.Args,
"--tun=userspace-networking",
)
}
cmd.Env = append(os.Environ(),
"TS_DEBUG_PERMIT_HTTP_C2N=1",
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
@@ -1065,6 +1117,9 @@ func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
"TS_LOGS_DIR="+t.TempDir(),
"TS_NETCHECK_GENERATE_204_URL="+n.env.ControlServer.URL+"/generate_204",
)
if version.IsRace() {
cmd.Env = append(cmd.Env, "GORACE=halt_on_error=1")
}
cmd.Stderr = &nodeOutputParser{n: n}
if *verboseTailscaled {
cmd.Stdout = os.Stdout
@@ -1136,11 +1191,10 @@ func (n *testNode) AwaitListening() {
s := safesocket.DefaultConnectionStrategy(n.sockFile)
if err := tstest.WaitFor(20*time.Second, func() (err error) {
c, err := safesocket.Connect(s)
if err != nil {
return err
if err == nil {
c.Close()
}
c.Close()
return nil
return err
}); err != nil {
t.Fatal(err)
}
@@ -1234,7 +1288,8 @@ func (n *testNode) AwaitNeedsLogin() {
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
// It does not start the process.
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
cmd := exec.Command(n.env.cli, "--socket="+n.sockFile)
cmd := exec.Command(n.env.cli)
cmd.Args = append(cmd.Args, "--socket="+n.sockFile)
cmd.Args = append(cmd.Args, arg...)
cmd.Dir = n.dir
cmd.Env = append(os.Environ(),

View File

@@ -16,6 +16,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"

View File

@@ -16,6 +16,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"

View File

@@ -16,6 +16,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"

View File

@@ -16,6 +16,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"

View File

@@ -23,6 +23,7 @@ import (
_ "tailscale.com/derp/derphttp"
_ "tailscale.com/envknob"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/ipnserver"
_ "tailscale.com/ipn/store"

View File

@@ -13,8 +13,19 @@ import (
"github.com/google/go-cmp/cmp"
)
// ResourceCheck takes a snapshot of the current goroutines and registers a
// cleanup on tb to verify that after the rest, all goroutines created by the
// test go away. (well, at least that the count matches. Maybe in the future it
// can look at specific routines).
//
// It panics if called from a parallel test.
func ResourceCheck(tb testing.TB) {
tb.Helper()
// Set an environment variable (anything at all) just for the
// side effect of tb.Setenv panicking if we're in a parallel test.
tb.Setenv("TS_CHECKING_RESOURCES", "1")
startN, startStacks := goroutines()
tb.Cleanup(func() {
if tb.Failed() {

View File

@@ -60,6 +60,46 @@ func Sleep(ctx context.Context, d time.Duration) bool {
}
}
// DefaultClock is a wrapper around a Clock.
// It uses StdClock by default if Clock is nil.
type DefaultClock struct{ Clock }
// TODO: We should make the methods of DefaultClock inlineable
// so that we can optimize for the common case where c.Clock == nil.
func (c DefaultClock) Now() time.Time {
if c.Clock == nil {
return time.Now()
}
return c.Clock.Now()
}
func (c DefaultClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) {
if c.Clock == nil {
t := time.NewTimer(d)
return t, t.C
}
return c.Clock.NewTimer(d)
}
func (c DefaultClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) {
if c.Clock == nil {
t := time.NewTicker(d)
return t, t.C
}
return c.Clock.NewTicker(d)
}
func (c DefaultClock) AfterFunc(d time.Duration, f func()) TimerController {
if c.Clock == nil {
return time.AfterFunc(d, f)
}
return c.Clock.AfterFunc(d, f)
}
func (c DefaultClock) Since(t time.Time) time.Duration {
if c.Clock == nil {
return time.Since(t)
}
return c.Clock.Since(t)
}
// Clock offers a subset of the functionality from the std/time package.
// Normally, applications will use the StdClock implementation that calls the
// appropriate std/time exported funcs. The advantage of using Clock is that

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