Compare commits

...

83 Commits

Author SHA1 Message Date
Denton Gentry
f8497daa68 VERSION.txt: this is v1.32.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-21 11:37:06 -07:00
Maisem Ali
8023971bff wgengine/router: [linux] add before deleting interface addrs
Deleting may temporarily result in no addrs on the interface, which results in
all other rules (like routes) to get dropped by the OS.

I verified this fixes the problem.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 74637f2c15)
2022-10-21 08:16:18 -07:00
Andrew Dunham
0cc397e96d cmd/derper, net/netcheck: add challenge/response to generate_204 endpoint
The Lufthansa in-flight wifi generates a synthetic 204 response to the
DERP server's /generate_204 endpoint. This PR adds a basic
challenge/response to the endpoint; something sufficiently complicated
that it's unlikely to be implemented by a captive portal. We can then
check for the expected response to verify whether we're being MITM'd.

Follow-up to #5601

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I94a68c9a16a7be7290200eea6a549b64f02ff48f
(cherry picked from commit 223126fe5b)
2022-10-21 08:16:18 -07:00
Anton Tolchanov
46235b790d net/interfaces: improve default route detection
Instead of treating any interface with a non-ifscope route as a
potential default gateway, now verify that a given route is
actually a default route (0.0.0.0/0 or ::/0).

Fixes #5879

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

Updates #5921

Change-Id: I1597644e8ba47b413e33f2201eab935145566c0e
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit 9d04ffc782)
2022-10-21 08:16:18 -07:00
Brad Fitzpatrick
7c2fdcd028 ipn/ipnlocal: fix E.G.G. port number accounting
Change-Id: Id35461fdde79448372271ba54f6e6af586f2304d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 9475801ebe)
2022-10-21 08:16:18 -07:00
Xe Iaso
613d624bea tsnet/examples/tshello: update example for LocalClient method (#5966)
Before this would silently fail if this program was running on a machine
that was not already running Tailscale. This patch changes the WhoIs
call to use the tsnet.Server LocalClient instead of the global tailscale
LocalClient.

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

Change-Id: Ieb830fbce81292acc4c3b4d1b675aa10766a18dc
Signed-off-by: Xe <xe@tailscale.com>
(cherry picked from commit 86c5bddce2)
2022-10-21 08:16:18 -07:00
Andrew Dunham
d982963e0b control/controlhttp: try to avoid flakes in TestDialPlan
Updates tailscale/corp#7446

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ifcf3b5176f065c2e67cbb8943f6356dea720a9c5
(cherry picked from commit a4e707bcf0)
2022-10-21 08:16:18 -07:00
Maisem Ali
cdf7ae8066 kube: handle 201 as a valid status code.
Fixes tailscale/corp#7478

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit af966391c7)
2022-10-21 08:16:18 -07:00
Denton Gentry
30afe38cb9 cmd/tailscale: correct --cpu-profile help text
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
(cherry picked from commit 19dfdeb1bb)
2022-10-21 08:16:18 -07:00
Andrew Dunham
2a6afafc76 cmd/tailscale, ipn: enable debug logs when --report flag is passed to bugreport (#5830)
Change-Id: Id22e9f4a2dcf35cecb9cd19dd844389e38c922ec
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
(cherry picked from commit c32f9f5865)
2022-10-21 08:16:18 -07:00
Tom DNetto
23a664325e ipn/ipnlocal: make tkaSyncIfNeeded exclusive with a mutex
Running corp/ipn#TestNetworkLockE2E has a 1/300 chance of failing, and
deskchecking suggests thats whats happening are two netmaps are racing each
other to be processed through tkaSyncIfNeededLocked. This happens in the
first place because we release b.mu during network RPCs.

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

Signed-off-by: Tom DNetto <tom@tailscale.com>
(cherry picked from commit a515fc517b)
2022-10-21 08:16:18 -07:00
Brad Fitzpatrick
b9e1c18578 net/netcheck: fix crash in checkCaptivePortal
If netcheck happens before there's a derpmap.

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

Change-Id: I51e0dfca8e40623e04702bc9cc471770ca20d2c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 9a264dac01)
2022-10-21 08:16:18 -07:00
James Tucker
a5340a07cf wgengine/router: fix MTU configuration on Windows
Always set the MTU to the Tailscale default MTU. In practice we are
missing applying an MTU for IPv6 on Windows prior to this patch.

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

Fixes #5914

Signed-off-by: James Tucker <james@tailscale.com>
(cherry picked from commit 4ec6d41682)
2022-10-21 08:16:18 -07:00
Joe Tsai
ccca9faaf8 wgengine: fix typo in Engine.PeerForIP (#5912)
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
(cherry picked from commit 49bae7fd5c)
2022-10-21 08:16:18 -07:00
Sonia Appasamy
f7c15dd0b0 types/view: add ContainsNonExitSubnetRoutes func
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
(cherry picked from commit 5363a90272)
2022-10-21 08:16:18 -07:00
Mihai Parparita
a780929391 derp/derphttp: fix nil pointer dereference when closing a netcheck client
NewNetcheckClient only initializes a subset of fields of derphttp.Client,
and the Close() call added by #5707 was result in a nil pointer dereference.
Make Close() safe to call when using NewNetcheckClient() too.

Fixes #5919

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit b2855cfd86)
2022-10-13 11:50:39 -07:00
Denton Gentry
fc688fe024 VERSION.txt: this is v1.32.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-12 09:28:49 -07:00
Brad Fitzpatrick
8a187159b2 cmd/ssh-auth-none-demo: add demo SSH server that acts like Tailscale SSH
For SSH client authors to fix their clients without setting up
Tailscale stuff.

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

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

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

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

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

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

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

Fixes #5103

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

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

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

Updates tailscale/corp#7304

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Updates #1548

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

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

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

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

Fixes #5743

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

Fixes #5790

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

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

Updates #2452

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Caught by tests in another repo.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-09-25 16:23:55 -07:00
180 changed files with 6350 additions and 1216 deletions

View File

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

View File

@@ -1 +1 @@
1.31.0
1.32.1

View File

@@ -24,11 +24,17 @@ func New(socket string) (*BIRDClient, error) {
return newWithTimeout(socket, responseTimeout)
}
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
}
defer func() {
if err != nil {
conn.Close()
}
}()
b := &BIRDClient{
socket: socket,
conn: conn,

View File

@@ -106,10 +106,10 @@ func TestChirp(t *testing.T) {
t.Fatal(err)
}
if err := c.EnableProtocol("rando"); err == nil {
t.Fatalf("enabling %q succeded", "rando")
t.Fatalf("enabling %q succeeded", "rando")
}
if err := c.DisableProtocol("rando"); err == nil {
t.Fatalf("disabling %q succeded", "rando")
t.Fatalf("disabling %q succeeded", "rando")
}
}

View File

@@ -459,7 +459,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode)
return nil, fmt.Errorf("control api responded with %d status code", resp.StatusCode)
}
// The test ran without fail

View File

@@ -267,15 +267,77 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
// BugReportOpts contains options to pass to the Tailscale daemon when
// generating a bug report.
type BugReportOpts struct {
// Note contains an optional user-provided note to add to the logs.
Note string
// Diagnose specifies whether to print additional diagnostic information to
// the logs when generating this bugreport.
Diagnose bool
// Record specifies, if non-nil, whether to perform a bugreport
// "recording"generating an initial log marker, then waiting for
// this channel to be closed before finishing the request, which
// generates another log marker.
Record <-chan struct{}
}
// BugReportWithOpts logs and returns a log marker that can be shared by the
// user with support.
//
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
qparams := make(url.Values)
if opts.Note != "" {
qparams.Set("note", opts.Note)
}
if opts.Diagnose {
qparams.Set("diagnose", "true")
}
if opts.Record != nil {
qparams.Set("record", "true")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var requestBody io.Reader
if opts.Record != nil {
pr, pw := io.Pipe()
requestBody = pr
// This goroutine waits for the 'Record' channel to be closed,
// and then closes the write end of our pipe to unblock the
// reader.
go func() {
defer pw.Close()
select {
case <-opts.Record:
case <-ctx.Done():
}
}()
}
// lc.send might block if opts.Record != nil; see above.
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
// BugReport logs and returns a log marker that can be shared by the user with support.
//
// This is the same as calling BugReportWithOpts and only specifying the Note
// field.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
@@ -286,6 +348,28 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// SetComponentDebugLogging sets component's debug logging enabled for
// the provided duration. If the duration is in the past, the debug logging
// is disabled.
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
var res struct {
Error string
}
if err := json.Unmarshal(body, &res); err != nil {
return err
}
if res.Error != "" {
return errors.New(res.Error)
}
return nil
}
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
@@ -642,14 +726,14 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
return &cert, nil
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
@@ -716,6 +800,30 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ip
return pr, nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 200, &b)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
pr := new(ipnstate.NetworkLockStatus)
if err := json.Unmarshal(body, pr); err != nil {
return nil, err
}
return pr, nil
}
// tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections.
//

View File

@@ -115,7 +115,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.httpClient().Do(req)
}
// sendRequest add the authenication key to the request and sends it. It
// sendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
if !I_Acknowledge_This_API_Is_Unstable {

View File

@@ -47,6 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/paths from tailscale.com/client/tailscale
tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
@@ -107,6 +108,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+

View File

@@ -325,11 +325,31 @@ func main() {
}
}
const (
noContentChallengeHeader = "X-Tailscale-Challenge"
noContentResponseHeader = "X-Tailscale-Response"
)
// For captive portal detection
func serveNoContent(w http.ResponseWriter, r *http.Request) {
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
badChar := strings.IndexFunc(challenge, func(r rune) bool {
return !isChallengeChar(r)
}) != -1
if len(challenge) <= 64 && !badChar {
w.Header().Set(noContentResponseHeader, "response "+challenge)
}
}
w.WriteHeader(http.StatusNoContent)
}
func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
}
// probeHandler is the endpoint that js/wasm clients hit to measure
// DERP latency, since they can't do UDP STUN queries.
func probeHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,6 +7,9 @@ package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tailscale.com/net/stun"
@@ -67,3 +70,57 @@ func BenchmarkServerSTUN(b *testing.B) {
}
}
func TestNoContent(t *testing.T) {
testCases := []struct {
name string
input string
want string
}{
{
name: "no challenge",
},
{
name: "valid challenge",
input: "input",
want: "response input",
},
{
name: "invalid challenge",
input: "foo\x00bar",
want: "",
},
{
name: "whitespace invalid challenge",
input: "foo bar",
want: "",
},
{
name: "long challenge",
input: strings.Repeat("x", 65),
want: "",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
if tt.input != "" {
req.Header.Set(noContentChallengeHeader, tt.input)
}
w := httptest.NewRecorder()
serveNoContent(w, req)
resp := w.Result()
if tt.want == "" {
if h, found := resp.Header[noContentResponseHeader]; found {
t.Errorf("got %+v; expected no response header", h)
}
return
}
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}

View File

@@ -13,6 +13,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/net/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
@@ -23,7 +24,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
up := strings.ToLower(r.Header.Get("Upgrade"))
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
base.ServeHTTP(w, r)
@@ -50,7 +51,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return
}
counterWebSocketAccepts.Add(1)
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
})

42
cmd/pgproxy/README.md Normal file
View File

@@ -0,0 +1,42 @@
# pgproxy
The pgproxy server is a proxy for the Postgres wire protocol. [Read
more in our blog
post](https://tailscale.com/blog/introducing-pgproxy/) about it!
The proxy runs an in-process Tailscale instance, accepts postgres
client connections over Tailscale only, and proxies them to the
configured upstream postgres server.
This proxy exists because postgres clients default to very insecure
connection settings: either they "prefer" but do not require TLS; or
they set sslmode=require, which merely requires that a TLS handshake
took place, but don't verify the server's TLS certificate or the
presented TLS hostname. In other words, sslmode=require enforces that
a TLS session is created, but that session can trivially be
machine-in-the-middled to steal credentials, data, inject malicious
queries, and so forth.
Because this flaw is in the client's validation of the TLS session,
you have no way of reliably detecting the misconfiguration
server-side. You could fix the configuration of all the clients you
know of, but the default makes it very easy to accidentally regress.
Instead of trying to verify client configuration over time, this proxy
removes the need for postgres clients to be configured correctly: the
upstream database is configured to only accept connections from the
proxy, and the proxy is only available to clients over Tailscale.
Therefore, clients must use the proxy to connect to the database. The
client<>proxy connection is secured end-to-end by Tailscale, which the
proxy enforces by verifying that the connecting client is a known
current Tailscale peer. The proxy<>server connection is established by
the proxy itself, using strict TLS verification settings, and the
client is only allowed to communicate with the server once we've
established that the upstream connection is safe to use.
A couple side benefits: because clients can only connect via
Tailscale, you can use Tailscale ACLs as an extra layer of defense on
top of the postgres user/password authentication. And, the proxy can
maintain an audit log of who connected to the database, complete with
the strongly authenticated Tailscale identity of the client.

366
cmd/pgproxy/pgproxy.go Normal file
View File

@@ -0,0 +1,366 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The pgproxy server is a proxy for the Postgres wire protocol.
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"expvar"
"flag"
"fmt"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/metrics"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
"tailscale.com/types/logger"
)
var (
hostname = flag.String("hostname", "", "Tailscale hostname to serve on")
port = flag.Int("port", 5432, "Listening port for client connections")
debugPort = flag.Int("debug-port", 80, "Listening port for debug/metrics endpoint")
upstreamAddr = flag.String("upstream-addr", "", "Address of the upstream Postgres server, in host:port format")
upstreamCA = flag.String("upstream-ca-file", "", "File containing the PEM-encoded CA certificate for the upstream server")
tailscaleDir = flag.String("state-dir", "", "Directory in which to store the Tailscale auth state")
)
func main() {
flag.Parse()
if *hostname == "" {
log.Fatal("missing --hostname")
}
if *upstreamAddr == "" {
log.Fatal("missing --upstream-addr")
}
if *upstreamCA == "" {
log.Fatal("missing --upstream-ca-file")
}
if *tailscaleDir == "" {
log.Fatal("missing --state-dir")
}
ts := &tsnet.Server{
Dir: *tailscaleDir,
Hostname: *hostname,
// Make the stdout logs a clean audit log of connections.
Logf: logger.Discard,
}
if os.Getenv("TS_AUTHKEY") == "" {
log.Print("Note: you need to run this with TS_AUTHKEY=... the first time, to join your tailnet of choice.")
}
tsclient, err := ts.LocalClient()
if err != nil {
log.Fatalf("getting tsnet API client: %v", err)
}
p, err := newProxy(*upstreamAddr, *upstreamCA, tsclient)
if err != nil {
log.Fatal(err)
}
expvar.Publish("pgproxy", p.Expvar())
if *debugPort != 0 {
mux := http.NewServeMux()
tsweb.Debugger(mux)
srv := &http.Server{
Handler: mux,
}
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
if err != nil {
log.Fatal(err)
}
go func() {
log.Fatal(srv.Serve(dln))
}()
}
ln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
log.Printf("serving access to %s on port %d", *upstreamAddr, *port)
log.Fatal(p.Serve(ln))
}
// proxy is a postgres wire protocol proxy, which strictly enforces
// the security of the TLS connection to its upstream regardless of
// what the client's TLS configuration is.
type proxy struct {
upstreamAddr string // "my.database.com:5432"
upstreamHost string // "my.database.com"
upstreamCertPool *x509.CertPool
downstreamCert []tls.Certificate
client *tailscale.LocalClient
activeSessions expvar.Int
startedSessions expvar.Int
errors metrics.LabelMap
}
// newProxy returns a proxy that forwards connections to
// upstreamAddr. The upstream's TLS session is verified using the CA
// cert(s) in upstreamCAPath.
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
bs, err := os.ReadFile(upstreamCAPath)
if err != nil {
return nil, err
}
upstreamCertPool := x509.NewCertPool()
if !upstreamCertPool.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("invalid CA cert in %q", upstreamCAPath)
}
h, _, err := net.SplitHostPort(upstreamAddr)
if err != nil {
return nil, err
}
downstreamCert, err := mkSelfSigned(h)
if err != nil {
return nil, err
}
return &proxy{
upstreamAddr: upstreamAddr,
upstreamHost: h,
upstreamCertPool: upstreamCertPool,
downstreamCert: []tls.Certificate{downstreamCert},
client: client,
errors: metrics.LabelMap{Label: "kind"},
}, nil
}
// Expvar returns p's monitoring metrics.
func (p *proxy) Expvar() expvar.Var {
ret := &metrics.Set{}
ret.Set("sessions_active", &p.activeSessions)
ret.Set("sessions_started", &p.startedSessions)
ret.Set("session_errors", &p.errors)
return ret
}
// Serve accepts postgres client connections on ln and proxies them to
// the configured upstream. ln can be any net.Listener, but all client
// connections must originate from tailscale IPs that can be verified
// with WhoIs.
func (p *proxy) Serve(ln net.Listener) error {
var lastSessionID int64
for {
c, err := ln.Accept()
if err != nil {
return err
}
id := time.Now().UnixNano()
if id == lastSessionID {
// Bluntly enforce SID uniqueness, even if collisions are
// fantastically unlikely (but OSes vary in how much timer
// precision they expose to the OS, so id might be rounded
// e.g. to the same millisecond)
id++
}
lastSessionID = id
go func(sessionID int64) {
if err := p.serve(sessionID, c); err != nil {
log.Printf("%d: session ended with error: %v", sessionID, err)
}
}(id)
}
}
var (
// sslStart is the magic bytes that postgres clients use to indicate
// that they want to do a TLS handshake. Servers should respond with
// the single byte "S" before starting a normal TLS handshake.
sslStart = [8]byte{0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f}
// plaintextStart is the magic bytes that postgres clients use to
// indicate that they're starting a plaintext authentication
// handshake.
plaintextStart = [8]byte{0, 0, 0, 86, 0, 3, 0, 0}
)
// serve proxies the postgres client on c to the proxy's upstream,
// enforcing strict TLS to the upstream.
func (p *proxy) serve(sessionID int64, c net.Conn) error {
defer c.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
whois, err := p.client.WhoIs(ctx, c.RemoteAddr().String())
if err != nil {
p.errors.Add("whois-failed", 1)
return fmt.Errorf("getting client identity: %v", err)
}
// Before anything else, log the connection attempt.
user, machine := "", ""
if whois.Node != nil {
if whois.Node.Hostinfo.ShareeNode() {
machine = "external-device"
} else {
machine = strings.TrimSuffix(whois.Node.Name, ".")
}
}
if whois.UserProfile != nil {
user = whois.UserProfile.LoginName
if user == "tagged-devices" && whois.Node != nil {
user = strings.Join(whois.Node.Tags, ",")
}
}
if user == "" || machine == "" {
p.errors.Add("no-ts-identity", 1)
return fmt.Errorf("couldn't identify source user and machine (user %q, machine %q)", user, machine)
}
log.Printf("%d: session start, from %s (machine %s, user %s)", sessionID, c.RemoteAddr(), machine, user)
start := time.Now()
defer func() {
elapsed := time.Since(start)
log.Printf("%d: session end, from %s (machine %s, user %s), lasted %s", sessionID, c.RemoteAddr(), machine, user, elapsed.Round(time.Millisecond))
}()
// Read the client's opening message, to figure out if it's trying
// to TLS or not.
var buf [8]byte
if _, err := io.ReadFull(c, buf[:len(sslStart)]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("initial magic read: %v", err)
}
var clientIsTLS bool
switch {
case buf == sslStart:
clientIsTLS = true
case buf == plaintextStart:
clientIsTLS = false
default:
p.errors.Add("client-bad-protocol", 1)
return fmt.Errorf("unrecognized initial packet = % 02x", buf)
}
// Dial & verify upstream connection.
var d net.Dialer
d.Timeout = 10 * time.Second
upc, err := d.Dial("tcp", p.upstreamAddr)
if err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream dial: %v", err)
}
defer upc.Close()
if _, err := upc.Write(sslStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("upstream write of start-ssl magic: %v", err)
}
if _, err := io.ReadFull(upc, buf[:1]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("reading upstream start-ssl response: %v", err)
}
if buf[0] != 'S' {
p.errors.Add("upstream-bad-protocol", 1)
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
}
tlsConf := &tls.Config{
ServerName: p.upstreamHost,
RootCAs: p.upstreamCertPool,
MinVersion: tls.VersionTLS12,
}
uptc := tls.Client(upc, tlsConf)
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("upstream-tls", 1)
return fmt.Errorf("upstream TLS handshake: %v", err)
}
// Accept the client conn and set it up the way the client wants.
var clientConn net.Conn
if clientIsTLS {
io.WriteString(c, "S") // yeah, we're good to speak TLS
s := tls.Server(c, &tls.Config{
ServerName: p.upstreamHost,
Certificates: p.downstreamCert,
MinVersion: tls.VersionTLS12,
})
if err = uptc.HandshakeContext(ctx); err != nil {
p.errors.Add("client-tls", 1)
return fmt.Errorf("client TLS handshake: %v", err)
}
clientConn = s
} else {
// Repeat the header we read earlier up to the server.
if _, err := uptc.Write(plaintextStart[:]); err != nil {
p.errors.Add("network-error", 1)
return fmt.Errorf("sending initial client bytes to upstream: %v", err)
}
clientConn = c
}
// Finally, proxy the client to the upstream.
errc := make(chan error, 1)
go func() {
_, err := io.Copy(uptc, clientConn)
errc <- err
}()
go func() {
_, err := io.Copy(clientConn, uptc)
errc <- err
}()
if err := <-errc; err != nil {
// Don't increment error counts here, because the most common
// cause of termination is client or server closing the
// connection normally, and it'll obscure "interesting"
// handshake errors.
return fmt.Errorf("session terminated with error: %v", err)
}
return nil
}
// mkSelfSigned creates and returns a self-signed TLS certificate for
// hostname.
func mkSelfSigned(hostname string) (tls.Certificate, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
if err != nil {
return tls.Certificate{}, err
}
pub := priv.Public()
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"pgproxy"},
},
DNSNames: []string{hostname},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(crand.Reader, &template, &template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{derBytes},
PrivateKey: priv,
Leaf: cert,
}, nil
}

View File

@@ -0,0 +1,171 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ssh-auth-none-demo is a demo SSH server that's meant to run on the
// public internet and highlight the unique parts of the Tailscale SSH
// server so SSH client authors can hit it easily and fix their SSH
// clients without needing to set up Tailscale and Tailscale SSH.
package main
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
// keyTypes are the SSH key types that we either try to read from the
// system's OpenSSH keys.
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
var (
addr = flag.String("addr", ":2222", "address to listen on")
)
func main() {
flag.Parse()
cacheDir, err := os.UserCacheDir()
if err != nil {
log.Fatal(err)
}
dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatal(err)
}
keys, err := getHostKeys(dir)
if err != nil {
log.Fatal(err)
}
if len(keys) == 0 {
log.Fatal("no host keys")
}
srv := &ssh.Server{
Addr: *addr,
Version: "Tailscale",
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
ImplicitAuthMethod: "tailscale",
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(gossh.ConnMetadata) (*gossh.Permissions, error) {
return nil, nil
},
BannerCallback: func(cm gossh.ConnMetadata) string {
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
},
}
},
}
for _, signer := range keys {
srv.AddHostKey(signer)
}
log.Printf("Running on %s ...", srv.Addr)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
log.Printf("done")
}
func handleSessionPostSSHAuth(s ssh.Session) {
log.Printf("Started session from user %q", s.User())
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
// Abort the session on Control-C or Control-D.
go func() {
buf := make([]byte, 1024)
for {
n, err := s.Read(buf)
for _, b := range buf[:n] {
if b <= 4 { // abort on Control-C (3) or Control-D (4)
io.WriteString(s, "bye\n")
s.Exit(1)
}
}
if err != nil {
return
}
}
}()
for i := 10; i > 0; i-- {
fmt.Fprintf(s, "%v ...\n", i)
time.Sleep(time.Second)
}
s.Exit(0)
}
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
for _, typ := range keyTypes {
hostKey, err := hostKeyFileOrCreate(dir, typ)
if err != nil {
return nil, err
}
signer, err := gossh.ParsePrivateKey(hostKey)
if err != nil {
return nil, err
}
ret = append(ret, signer)
}
return ret, nil
}
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
v, err := ioutil.ReadFile(path)
if err == nil {
return v, nil
}
if !os.IsNotExist(err) {
return nil, err
}
var priv any
switch typ {
default:
return nil, fmt.Errorf("unsupported key type %q", typ)
case "ed25519":
_, priv, err = ed25519.GenerateKey(rand.Reader)
case "ecdsa":
// curve is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
curve := elliptic.P256()
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
case "rsa":
// keySize is arbitrary. We pick whatever will at
// least pacify clients as the actual encryption
// doesn't matter: it's all over WireGuard anyway.
const keySize = 2048
priv, err = rsa.GenerateKey(rand.Reader, keySize)
}
if err != nil {
return nil, err
}
mk, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
err = os.WriteFile(path, pemGen, 0700)
return pemGen, err
}

View File

@@ -7,8 +7,11 @@ package cli
import (
"context"
"errors"
"flag"
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
@@ -16,6 +19,17 @@ var bugReportCmd = &ffcli.Command{
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
fs.BoolVar(&bugReportArgs.record, "record", false, "if true, pause and then write another bugreport")
return fs
})(),
}
var bugReportArgs struct {
diagnose bool
record bool
}
func runBugReport(ctx context.Context, args []string) error {
@@ -25,12 +39,46 @@ func runBugReport(ctx context.Context, args []string) error {
case 1:
note = args[0]
default:
return errors.New("unknown argumets")
return errors.New("unknown arguments")
}
logMarker, err := localClient.BugReport(ctx, note)
if err != nil {
return err
opts := tailscale.BugReportOpts{
Note: note,
Diagnose: bugReportArgs.diagnose,
}
outln(logMarker)
if !bugReportArgs.record {
// Simple, non-record case
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
if err != nil {
return err
}
outln(logMarker)
return nil
}
// Recording; run the request in the background
done := make(chan struct{})
opts.Record = done
type bugReportResp struct {
marker string
err error
}
resCh := make(chan bugReportResp, 1)
go func() {
m, err := localClient.BugReportWithOpts(ctx, opts)
resCh <- bugReportResp{m, err}
}()
outln("Recording started; please reproduce your issue and then press Enter...")
fmt.Scanln()
close(done)
res := <-resCh
if res.err != nil {
return res.err
}
outln(res.marker)
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
return nil
}

View File

@@ -410,7 +410,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
},
{
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
flags: []string{"--hostname=foo"},
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
curPrefs: &ipn.Prefs{
@@ -448,7 +448,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
},
{
// Issue 3176: on Synology, don't require --accept-routes=false because user
// migth've had old an install, and we don't support --accept-routes anyway.
// might've had an old install, and we don't support --accept-routes anyway.
name: "synology_permit_omit_accept_routes",
flags: []string{"--hostname=foo"},
curPrefs: &ipn.Prefs{

View File

@@ -48,7 +48,7 @@ func runConfigureHost(ctx context.Context, args []string) error {
if uid := os.Getuid(); uid != 0 {
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
}
hi:= hostinfo.New()
hi := hostinfo.New()
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
if !isDSM6 && !isDSM7 {

View File

@@ -42,7 +42,7 @@ var debugCmd = &ffcli.Command{
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-seconds seconds and write it to this file; - for stdout")
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
return fs
@@ -53,6 +53,16 @@ var debugCmd = &ffcli.Command{
Exec: runDERPMap,
ShortHelp: "print DERP map",
},
{
Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
return fs
})(),
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
@@ -513,3 +523,26 @@ func runTS2021(ctx context.Context, args []string) error {
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
return nil
}
var debugComponentLogsArgs struct {
forDur time.Duration
}
func runDebugComponentLogs(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug component-logs <component>")
}
component := args[0]
dur := debugComponentLogsArgs.forDur
err := localClient.SetComponentDebugLogging(ctx, component, dur)
if err != nil {
return err
}
if debugComponentLogsArgs.forDur <= 0 {
fmt.Printf("Disabled debug logs for component %q\n", component)
} else {
fmt.Printf("Enabled debug logs for component %q for %v\n", component, dur)
}
return nil
}

View File

@@ -17,11 +17,16 @@ import (
)
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manipulate the tailnet key authority",
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
Exec: runNetworkLockStatus,
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortHelp: "Manipulate the tailnet key authority",
Subcommands: []*ffcli.Command{
nlInitCmd,
nlStatusCmd,
nlAddCmd,
nlRemoveCmd,
},
Exec: runNetworkLockStatus,
}
var nlInitCmd = &ffcli.Command{
@@ -41,29 +46,9 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
}
// Parse the set of initially-trusted keys.
// Keys are specified using their key.NLPublic.MarshalText representation,
// with an optional '?<votes>' suffix.
var keys []tka.Key
for i, a := range args {
var key key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
return fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: key.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
keys, err := parseNLKeyArgs(args)
if err != nil {
return err
}
status, err := localClient.NetworkLockInit(ctx, keys)
@@ -99,3 +84,78 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
fmt.Printf("our public-key: %s\n", p)
return nil
}
var nlAddCmd = &ffcli.Command{
Name: "add",
ShortUsage: "add <public-key>...",
ShortHelp: "Adds one or more signing keys to the tailnet key authority",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, args, nil)
},
}
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove <public-key>...",
ShortHelp: "Removes one or more signing keys to the tailnet key authority",
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, nil, args)
},
}
// parseNLKeyArgs converts a slice of strings into a slice of tka.Key. The keys
// should be specified using their key.NLPublic.MarshalText representation with
// an optional '?<votes>' suffix. If any of the keys encounters an error, a nil
// slice is returned along with an appropriate error.
func parseNLKeyArgs(args []string) ([]tka.Key, error) {
var keys []tka.Key
for i, a := range args {
var nlpk key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
return nil, fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
Kind: tka.Key25519,
Public: nlpk.Verifier(),
Votes: 1,
}
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
return keys, nil
}
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
if st.Enabled {
return errors.New("network-lock is already enabled")
}
addKeys, err := parseNLKeyArgs(addArgs)
if err != nil {
return err
}
removeKeys, err := parseNLKeyArgs(removeArgs)
if err != nil {
return err
}
status, err := localClient.NetworkLockModify(ctx, addKeys, removeKeys)
if err != nil {
return err
}
fmt.Printf("Status: %+v\n\n", status)
return nil
}

View File

@@ -13,7 +13,7 @@ import (
func findSSH() (string, error) {
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
// occured with ssh.exe provided by msys2/cygwin and other environments.
// occurred with ssh.exe provided by msys2/cygwin and other environments.
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
if st, err := os.Stat(exe); err == nil && !st.IsDir() {

View File

@@ -501,7 +501,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
fatalf("%s", err)
}
if justEditMP != nil {
justEditMP.EggSet = true
justEditMP.EggSet = egg
_, err := localClient.EditPrefs(ctx, justEditMP)
return err
}

View File

@@ -70,6 +70,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/netcheck+
@@ -135,6 +136,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+

View File

@@ -109,7 +109,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
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
@@ -190,6 +190,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/hostinfo from tailscale.com/control/controlclient+
@@ -230,6 +232,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
@@ -237,6 +240,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/net/dns+
tailscale.com/net/tunstats from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/client/tailscale+
@@ -272,6 +277,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
LW tailscale.com/util/endian from tailscale.com/net/dns+
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/lineread from tailscale.com/hostinfo+
@@ -317,6 +323,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -338,7 +345,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled+
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+

View File

@@ -88,7 +88,7 @@ func defaultTunName() string {
// see https://github.com/tailscale/tailscale/issues/391
//
// But Gokrazy does have the tun module built-in, so users
// can stil run --tun=tailscale0 if they wish, if they
// can still run --tun=tailscale0 if they wish, if they
// arrange for iptables to be present or run in "tailscale
// up --netfilter-mode=off" mode, perhaps. Untested.
return "userspace-networking"
@@ -158,7 +158,7 @@ func main() {
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
@@ -375,8 +375,7 @@ func run() error {
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := new(tsdial.Dialer) // mutated below (before used)
dialer.Logf = logf
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
e, useNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return fmt.Errorf("createEngine: %w", err)

View File

@@ -23,6 +23,7 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/netip"
@@ -192,7 +193,7 @@ func beWindowsSubprocess() bool {
}
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file logggers add it.
// Remove the date/time prefix; the logtail + file loggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
@@ -265,11 +266,17 @@ func startIPNServer(ctx context.Context, logid string) error {
if err != nil {
return fmt.Errorf("monitor: %w", err)
}
dialer := new(tsdial.Dialer)
dialer := &tsdial.Dialer{Logf: logf}
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) {
// Wintun is not installing correctly. Dump the state of NetSetupSvc
// (which is a user-mode service that must be active for network devices
// to install) and its dependencies to the log.
winutil.LogSvcState(logf, "NetSetupSvc")
}
return nil, nil, fmt.Errorf("TUN: %w", err)
}
r, err := router.New(logf, dev, nil)

View File

@@ -57,7 +57,7 @@ func runBuild() {
// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
// relative to the dist directory (it normally uses paths relative to the cwd,
// which are akward if we're running with a different cwd at serving time).
// which are awkward if we're running with a different cwd at serving time).
func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
var metadata EsbuildMetadata
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {

View File

@@ -10,9 +10,9 @@
"qrcode": "^1.5.0",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4",
"xterm": "5.0.0-beta.58",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-web-links": "0.7.0-beta.6"
"xterm": "^5.0.0",
"xterm-addon-fit": "^0.6.0",
"xterm-addon-web-links": "^0.7.0"
},
"scripts": {
"lint": "tsc --noEmit",

View File

@@ -42,7 +42,7 @@ export function runSSHSession(
term.focus()
let resizeObserver: ResizeObserver | undefined
let handleBeforeUnload: ((e: BeforeUnloadEvent) => void) | undefined
let handleUnload: ((e: Event) => void) | undefined
const sshSession = ipn.ssh(def.hostname, def.username, {
writeFn(input) {
@@ -60,8 +60,8 @@ export function runSSHSession(
onDone() {
resizeObserver?.disconnect()
term.dispose()
if (handleBeforeUnload) {
parentWindow.removeEventListener("beforeunload", handleBeforeUnload)
if (handleUnload) {
parentWindow.removeEventListener("unload", handleUnload)
}
onDone()
},
@@ -75,6 +75,6 @@ export function runSSHSession(
// Close the session if the user closes the window without an explicit
// exit.
handleBeforeUnload = () => sshSession.close()
parentWindow.addEventListener("beforeunload", handleBeforeUnload)
handleUnload = () => sshSession.close()
parentWindow.addEventListener("unload", handleUnload)
}

View File

@@ -15,12 +15,12 @@ import wasmURL from "./main.wasm"
* needed for the package to function.
*/
type IPNPackageConfig = IPNConfig & {
// Auth key used to intitialize the Tailscale client (required)
// Auth key used to initialize the Tailscale client (required)
authKey: string
// URL of the main.wasm file that is included in the page, if it is not
// accessible via a relative URL.
wasmURL?: string
// Funtion invoked if the Go process panics or unexpectedly exits.
// Function invoked if the Go process panics or unexpectedly exits.
panicHandler: (err: string) => void
}

View File

@@ -96,7 +96,7 @@ func newIPN(jsConfig js.Value) map[string]any {
logtail := logtail.NewLogger(c, log.Printf)
logf := logtail.Logf
dialer := new(tsdial.Dialer)
dialer := &tsdial.Dialer{Logf: logf}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Dialer: dialer,
})

View File

@@ -639,20 +639,20 @@ xtend@^4.0.2:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
xterm-addon-fit@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
xterm-addon-fit@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.6.0.tgz#142e1ce181da48763668332593fc440349c88c34"
integrity sha512-9/7A+1KEjkFam0yxTaHfuk9LEvvTSBi0PZmEkzJqgafXPEXL9pCMAVV7rB09sX6ATRDXAdBpQhZkhKj7CGvYeg==
xterm@5.0.0-beta.58:
version "5.0.0-beta.58"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.58.tgz#e3e96ab9fd24d006ec16cc9351a060cc79e67e80"
integrity sha512-gjg39oKdgUKful27+7I1hvSK51lu/LRhdimFhfZyMvdk0iATH0FAfzv1eAvBKWY2UBgYUfxhicTkanYioANdMw==
xterm-addon-web-links@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0.tgz#dceac36170605f9db10a01d716bd83ee38f65c17"
integrity sha512-6PqoqzzPwaeSq22skzbvyboDvSnYk5teUYEoKBwMYvhbkwOQkemZccjWHT5FnNA8o1aInTc4PRYAl4jjPucCKA==
xterm-addon-web-links@0.7.0-beta.6:
version "0.7.0-beta.6"
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0-beta.6.tgz#ec63b681b4f0f0135fa039f53664f65fe9d9f43a"
integrity sha512-nD/r/GchGTN4c9gAIVLWVoxExTzAUV7E9xZnwsvhuwI4CEE6yqO15ns8g2hdcUrsPyCbNEw05mIrkF6W5Yj8qA==
xterm@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c"
integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA==
y18n@^4.0.0:
version "4.0.3"

View File

@@ -388,7 +388,7 @@ func main() {
log.Fatal(err)
}
if runCloner {
// When a new pacakge is added or when existing generated files have
// When a new package is added or when existing generated files have
// been deleted, we might run into a case where tailscale.com/cmd/cloner
// has not run yet. We detect this by verifying that all the structs we
// interacted with have had Clone method already generated. If they

View File

@@ -8,12 +8,11 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"log"
"net/http"
"runtime"
"strconv"
"time"
"tailscale.com/util/goroutines"
)
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
@@ -22,7 +21,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
zbuf := new(bytes.Buffer)
zw := gzip.NewWriter(zbuf)
zw.Write(scrubbedGoroutineDump())
zw.Write(goroutines.ScrubbedGoroutineDump())
zw.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
@@ -40,83 +39,3 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
}
}
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func scrubbedGoroutineDump() []byte {
var buf []byte
// Grab stacks multiple times into increasingly larger buffer sizes
// to minimize the risk that we blow past our iOS memory limit.
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
// It fit.
break
}
}
return scrubHex(buf)
}
func scrubHex(buf []byte) []byte {
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
foreachHexAddress(buf, func(in []byte) {
if string(in) == "0x0" {
return
}
if v, ok := saw[string(in)]; ok {
for i := range in {
in[i] = '_'
}
copy(in, v)
return
}
inStr := string(in)
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
for i := range in {
in[i] = '_'
}
if err != nil {
in[0] = '?'
return
}
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
saw[inStr] = v
copy(in, v)
})
return buf
}
var ohx = []byte("0x")
// foreachHexAddress calls f with each subslice of b that matches
// regexp `0x[0-9a-f]*`.
func foreachHexAddress(b []byte, f func([]byte)) {
for len(b) > 0 {
i := bytes.Index(b, ohx)
if i == -1 {
return
}
b = b[i:]
hx := hexPrefix(b)
f(hx)
b = b[len(hx):]
}
}
func hexPrefix(b []byte) []byte {
for i, c := range b {
if i < 2 {
continue
}
if !isHexByte(c) {
return b[:i]
}
}
return b
}
func isHexByte(b byte) bool {
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}

View File

@@ -776,7 +776,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// because the e2e tests are currently hyper-specific about the
// ordering of things. The e2e tests need love.
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
}
@@ -1438,7 +1438,7 @@ func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) er
if err != nil {
return err
}
res, err := nc.post(ctx, "/machine/set-dns", req)
res, err := nc.post(ctx, "/machine/set-dns", &newReq)
if err != nil {
return err
}

View File

@@ -35,7 +35,7 @@ type mapSession struct {
machinePubKey key.MachinePublic
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
// Fields storing state over the the coards of multiple MapResponses.
// Fields storing state over the course of multiple MapResponses.
lastNode *tailcfg.Node
lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap
@@ -45,6 +45,7 @@ type mapSession struct {
collectServices bool
previousPeers []*tailcfg.Node // for delta-purposes
lastDomain string
lastDomainAuditLogID string
lastHealth []string
lastPopBrowserURL string
stickyDebug tailcfg.Debug // accumulated opt.Bool values
@@ -113,6 +114,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
if resp.Domain != "" {
ms.lastDomain = resp.Domain
}
if resp.DomainDataPlaneAuditLogID != "" {
ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID
}
if resp.Health != nil {
ms.lastHealth = resp.Health
}
@@ -143,20 +147,21 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
}
nm := &netmap.NetworkMap{
NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: resp.Peers,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
PacketFilter: ms.lastParsedPacketFilter,
SSHPolicy: ms.lastSSHPolicy,
CollectServices: ms.collectServices,
DERPMap: ms.lastDERPMap,
Debug: debug,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
}
ms.netMapBuilding = nm

View File

@@ -466,7 +466,7 @@ func TestNetmapForResponse(t *testing.T) {
})
}
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResposnes
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResponses
// entirely or have their opt.Bool values unspecified between MapResponses in a
// session and that should mean no change. (as of capver 37). But two Debug
// fields existed prior to capver 37 that weren't opt.Bool; we test that we both

View File

@@ -13,6 +13,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/wsconn"
)
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
@@ -29,9 +30,11 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
wsScheme := "wss"
host := d.Hostname
if host == "localhost" {
// If using a custom control server (on a non-standard port), prefer that.
// This mirrors the port selection in newNoiseClient from noise.go.
if d.HTTPPort != "" && d.HTTPPort != "80" && d.HTTPSPort == "443" {
wsScheme = "ws"
host = net.JoinHostPort(host, strDef(d.HTTPPort, "80"))
host = net.JoinHostPort(host, d.HTTPPort)
}
wsURL := &url.URL{
Scheme: wsScheme,
@@ -49,7 +52,7 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
if err != nil {
return nil, err
}
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
netConn := wsconn.NetConn(context.Background(), wsConn, websocket.MessageBinary)
cbConn, err := cont(ctx, netConn)
if err != nil {
netConn.Close()

View File

@@ -459,13 +459,26 @@ func TestDialPlan(t *testing.T) {
const (
testProtocolVersion = 1
// We need consistent ports for each address; these are chosen
// randomly and we hope that they won't conflict during this test.
httpPort = "40080"
httpsPort = "40443"
)
getRandomPort := func() string {
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("net.Listen: %v", err)
}
defer ln.Close()
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatal(err)
}
return port
}
// We need consistent ports for each address; these are chosen
// randomly and we hope that they won't conflict during this test.
httpPort := getRandomPort()
httpsPort := getRandomPort()
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
done := make(chan struct{})
t.Cleanup(func() {

View File

@@ -14,6 +14,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
@@ -111,7 +112,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()

View File

@@ -232,7 +232,7 @@ func TestSendFreeze(t *testing.T) {
// alice --> bob
// alice --> cathy
//
// Then cathy stops processing messsages.
// Then cathy stops processing messages.
// That should not interfere with alice talking to bob.
newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
@@ -772,7 +772,7 @@ func TestForwarderRegistration(t *testing.T) {
})
// Now pretend u1 was already connected locally (so clientsMesh[u1] is nil), and then we heard
// that they're also connected to a peer of ours. That sholdn't transition the forwarder
// that they're also connected to a peer of ours. That shouldn't transition the forwarder
// from nil to the new one, not a multiForwarder.
s.clients[u1] = singleClient{u1c}
s.clientsMesh[u1] = nil

View File

@@ -96,7 +96,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion fun
return c
}
// NewNetcheckClient returns a Client that's only able to have its DialRegion method called.
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
// It's used by the netcheck package.
func NewNetcheckClient(logf logger.Logf) *Client {
return &Client{logf: logf}
@@ -199,7 +199,7 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
return fmt.Sprintf("https://%s/derp", node.HostName)
}
// AddressFamilySelector decides whethers IPv6 is preferred for
// AddressFamilySelector decides whether IPv6 is preferred for
// outbound dials.
type AddressFamilySelector interface {
// PreferIPv6 reports whether IPv4 dials should be slightly
@@ -985,7 +985,9 @@ func (c *Client) isClosed() bool {
// Close closes the client. It will not automatically reconnect after
// being closed.
func (c *Client) Close() error {
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
if c.cancelCtx != nil {
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
}
c.mu.Lock()
defer c.mu.Unlock()

View File

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

View File

@@ -15,9 +15,9 @@
// The recipient then decrypts the bytes following (the nacl secretbox)
// and then the inner payload structure is:
//
// messageType byte (the MessageType constants below)
// messageVersion byte (0 for now; but always ignore bytes at the end)
// message-paylod [...]byte
// messageType byte (the MessageType constants below)
// messageVersion byte (0 for now; but always ignore bytes at the end)
// message-payload [...]byte
package disco
import (

View File

@@ -9,7 +9,7 @@ spec:
serviceAccountName: "{{SA_NAME}}"
initContainers:
# In order to run as a proxy we need to enable IP Forwarding inside
# the container. The `net.ipv4.ip_forward` sysctl is not whitelisted
# the container. The `net.ipv4.ip_forward` sysctl is not allowlisted
# in Kubelet by default.
- name: sysctler
image: busybox
@@ -18,7 +18,7 @@ spec:
command: ["/bin/sh"]
args:
- -c
- sysctl -w net.ipv4.ip_forward=1 -w net.ipv6.conf.all.forwarding=1
- sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
resources:
requests:
cpu: 1m

80
doctor/doctor.go Normal file
View File

@@ -0,0 +1,80 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package doctor contains more in-depth healthchecks that can be run to aid in
// diagnosing Tailscale issues.
package doctor
import (
"context"
"sync"
"tailscale.com/types/logger"
)
// Check is the interface defining a singular check.
//
// A check should log information that it gathers using the provided log
// function, and should attempt to make as much progress as possible in error
// conditions.
type Check interface {
// Name should return a name describing this check, in lower-kebab-case
// (i.e. "my-check", not "MyCheck" or "my_check").
Name() string
// Run executes the check, logging diagnostic information to the
// provided logger function.
Run(context.Context, logger.Logf) error
}
// RunChecks runs a list of checks in parallel, and logs any returned errors
// after all checks have returned.
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
if len(checks) == 0 {
return
}
type namedErr struct {
name string
err error
}
errs := make(chan namedErr, len(checks))
var wg sync.WaitGroup
wg.Add(len(checks))
for _, check := range checks {
go func(c Check) {
defer wg.Done()
plog := logger.WithPrefix(log, c.Name()+": ")
errs <- namedErr{
name: c.Name(),
err: c.Run(ctx, plog),
}
}(check)
}
wg.Wait()
close(errs)
for n := range errs {
if n.err == nil {
continue
}
log("check %s: %v", n.name, n.err)
}
}
// CheckFunc creates a Check from a name and a function.
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
return checkFunc{name, run}
}
type checkFunc struct {
name string
run func(context.Context, logger.Logf) error
}
func (c checkFunc) Name() string { return c.name }
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }

50
doctor/doctor_test.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package doctor
import (
"context"
"fmt"
"sync"
"testing"
qt "github.com/frankban/quicktest"
"tailscale.com/types/logger"
)
func TestRunChecks(t *testing.T) {
c := qt.New(t)
var (
mu sync.Mutex
lines []string
)
logf := func(format string, args ...any) {
mu.Lock()
defer mu.Unlock()
lines = append(lines, fmt.Sprintf(format, args...))
}
ctx := context.Background()
RunChecks(ctx, logf,
testCheck1{},
CheckFunc("testcheck2", func(_ context.Context, log logger.Logf) error {
log("check 2")
return nil
}),
)
mu.Lock()
defer mu.Unlock()
c.Assert(lines, qt.Contains, "testcheck1: check 1")
c.Assert(lines, qt.Contains, "testcheck2: check 2")
}
type testCheck1 struct{}
func (t testCheck1) Name() string { return "testcheck1" }
func (t testCheck1) Run(_ context.Context, log logger.Logf) error {
log("check 1")
return nil
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package routetable provides a doctor.Check that dumps the current system's
// route table to the log.
package routetable
import (
"context"
"tailscale.com/net/routetable"
"tailscale.com/types/logger"
)
// MaxRoutes is the maximum number of routes that will be displayed.
const MaxRoutes = 1000
// Check implements the doctor.Check interface.
type Check struct{}
func (Check) Name() string {
return "routetable"
}
func (Check) Run(_ context.Context, logf logger.Logf) error {
rs, err := routetable.Get(MaxRoutes)
if err != nil {
return err
}
for _, r := range rs {
logf("%s", r)
}
return nil
}

View File

@@ -277,6 +277,11 @@ func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
// TKASkipSignatureCheck is whether to skip node-key signature checking for development.
func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION") }
// NoLogsNoSupport reports whether the client's opted out of log uploads and
// technical support.
func NoLogsNoSupport() bool {

9
go.mod
View File

@@ -44,7 +44,7 @@ require (
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
@@ -57,9 +57,9 @@ require (
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/net v0.0.0-20221002022538-bcab6841153b
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
golang.org/x/tools v0.1.11
@@ -210,7 +210,6 @@ require (
github.com/nishanths/exhaustive v0.7.11 // indirect
github.com/nishanths/predeclared v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
@@ -230,7 +229,7 @@ require (
github.com/ryancurrah/gomodguard v1.2.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
github.com/sassoftware/go-rpmutils v0.1.0 // indirect
github.com/securego/gosec/v2 v2.9.3 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect

16
go.sum
View File

@@ -876,7 +876,6 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@@ -990,8 +989,9 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc=
github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/sassoftware/go-rpmutils v0.1.0 h1:VLrna+tV+77Tclr956QkY/pTyyKomQlq2Xw6PuE8tsc=
github.com/sassoftware/go-rpmutils v0.1.0/go.mod h1:euhXULoBpvAxqrBHEyJS4Tsu3hHxUmQWNymxoJbzgUY=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo=
github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc=
@@ -1082,8 +1082,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986 h1:jWSwTR9CY13oa2oxhR3FInk1ybqC1NbF9cFeoWrrx+E=
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f h1:n4r/sJ92cBSBHK8n9lR1XLFr0OiTVeGfN5TR+9LaN7E=
@@ -1235,6 +1235,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@@ -1354,8 +1355,8 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
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=
@@ -1490,8 +1491,9 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=

View File

@@ -1 +1 @@
b13188dd36c1ad2509796ce10b6a1231b200c36a
3fd24dee31726924c1b61c8037a889b30b8aa0f6

View File

@@ -69,7 +69,7 @@ type Notify struct {
State *State // if non-nil, the new or current IPN state
Prefs *Prefs // if non-nil, the new or current preferences
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
Engine *EngineStatus // if non-nil, the new or current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
@@ -168,6 +168,11 @@ type PartialFile struct {
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// - on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
//
// Additionally, the StateKey can be debug setting name:
//
// - "_debug_magicsock_until" with value being a unix timestamp stringified
// - "_debug_<component>_until" with value being a unix timestamp stringified
type StateKey string
type Options struct {

View File

@@ -8,16 +8,47 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"time"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
)
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
writeJSON := func(v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
switch r.URL.Path {
case "/echo":
// Test handler.
body, _ := io.ReadAll(r.Body)
w.Write(body)
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump())
case "/debug/prefs":
writeJSON(b.Prefs())
case "/debug/metrics":
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
case "/debug/component-logging":
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
if secs == 0 {
secs -= 1
}
until := time.Now().Add(time.Duration(secs) * time.Second)
err := b.SetComponentDebugLogging(component, until)
var res struct {
Error string `json:",omitempty"`
}
if err != nil {
res.Error = err.Error()
}
writeJSON(res)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {
@@ -31,8 +62,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
writeJSON(res)
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}

View File

@@ -24,8 +24,11 @@ import (
"time"
"go4.org/netipx"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
"tailscale.com/doctor/routetable"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
@@ -53,6 +56,7 @@ import (
"tailscale.com/types/views"
"tailscale.com/util/deephash"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/util/systemd"
@@ -184,6 +188,7 @@ type LocalBackend struct {
// *.partial file to its final name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
componentLogUntil map[string]componentLogState
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -193,6 +198,14 @@ type LocalBackend struct {
// dialPlan is any dial plan that we've received from the control
// server during a previous connection; it is cleared on logout.
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
// tkaSyncLock is used to make tkaSyncIfNeeded an exclusive
// section. This is needed to stop two map-responses in quick succession
// from racing each other through TKA sync logic / RPCs.
//
// tkaSyncLock MUST be taken before mu (or inversely, mu must not be held
// at the moment that tkaSyncLock is taken).
tkaSyncLock sync.Mutex
}
// clientGen is a func that creates a control plane client.
@@ -212,7 +225,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
logf.JSON(1, "Hostinfo", hi)
envknob.LogCurrent(logf)
if dialer == nil {
dialer = new(tsdial.Dialer)
dialer = &tsdial.Dialer{Logf: logf}
}
osshare.SetFileSharingEnabled(false, logf)
@@ -265,9 +278,106 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
b.logf("[unexpected] failed to wire up peer API port for engine %T", e)
}
for _, component := range debuggableComponents {
key := componentStateKey(component)
if ut, err := ipn.ReadStoreInt(store, key); err == nil {
if until := time.Unix(ut, 0); until.After(time.Now()) {
// conditional to avoid log spam at start when off
b.SetComponentDebugLogging(component, until)
}
}
}
return b, nil
}
type componentLogState struct {
until time.Time
timer *time.Timer // if non-nil, the AfterFunc to disable it
}
var debuggableComponents = []string{
"magicsock",
}
func componentStateKey(component string) ipn.StateKey {
return ipn.StateKey("_debug_" + component + "_until")
}
// SetComponentDebugLogging sets component's debug logging enabled until the until time.
// If until is in the past, the component's debug logging is disabled.
//
// The following components are recognized:
//
// - magicsock
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
b.mu.Lock()
defer b.mu.Unlock()
var setEnabled func(bool)
switch component {
case "magicsock":
mc, err := b.magicConn()
if err != nil {
return err
}
setEnabled = mc.SetDebugLoggingEnabled
}
if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
return fmt.Errorf("unknown component %q", component)
}
timeUnixOrZero := func(t time.Time) int64 {
if t.IsZero() {
return 0
}
return t.Unix()
}
ipn.PutStoreInt(b.store, componentStateKey(component), timeUnixOrZero(until))
now := time.Now()
on := now.Before(until)
setEnabled(on)
var onFor time.Duration
if on {
onFor = until.Sub(now)
b.logf("debugging logging for component %q enabled for %v (until %v)", component, onFor.Round(time.Second), until.UTC().Format(time.RFC3339))
} else {
b.logf("debugging logging for component %q disabled", component)
}
if oldSt, ok := b.componentLogUntil[component]; ok && oldSt.timer != nil {
oldSt.timer.Stop()
}
newSt := componentLogState{until: until}
if on {
newSt.timer = time.AfterFunc(onFor, func() {
// Turn off logging after the timer fires, as long as the state is
// unchanged when the timer actually fires.
b.mu.Lock()
defer b.mu.Unlock()
if ls := b.componentLogUntil[component]; ls.until == until {
setEnabled(false)
b.logf("debugging logging for component %q disabled (by timer)", component)
}
})
}
mak.Set(&b.componentLogUntil, component, newSt)
return nil
}
// GetComponentDebugLogging gets the time that component's debug logging is
// enabled until, or the zero time if component's time is not currently
// enabled.
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
ls := b.componentLogUntil[component]
if ls.until.IsZero() || ls.until.Before(now) {
return time.Time{}
}
return ls.until
}
// Dialer returns the backend's dialer.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
@@ -688,9 +798,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
}
if st.NetMap != nil {
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
if err := b.tkaSyncIfNeeded(st.NetMap); err != nil {
b.logf("[v1] TKA sync error: %v", err)
}
b.mu.Lock()
if !envknob.TKASkipSignatureCheck() {
b.tkaFilterNetmapLocked(st.NetMap)
}
if b.findExitNodeIDLocked(st.NetMap) {
prefsChanged = true
}
@@ -2171,7 +2287,7 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
// ServePeerAPIConnection serves an already-accepted connection c.
//
// The remote parameter is the remote address.
// The local paramater is the local address (either a Tailscale IPv4
// The local parameter is the local address (either a Tailscale IPv4
// or IPv6 IP and the peerapi port for that address).
//
// The connection will be closed by ServePeerAPIConnection.
@@ -2231,7 +2347,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
}
peerAPIServices := b.peerAPIServicesLocked()
if b.egg {
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg"})
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg", Port: 1})
}
b.mu.Unlock()
@@ -3025,7 +3141,7 @@ func (b *LocalBackend) RequestEngineStatus() {
// that have happened. It is invoked from the various callbacks that
// feed events into LocalBackend.
//
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
// TODO(apenwarr): use a channel or something to prevent reentrancy?
// Or maybe just call the state machine from fewer places.
func (b *LocalBackend) stateMachine() {
b.enterState(b.nextState())
@@ -3085,7 +3201,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
// ShouldHandleViaIP reports whether ip is an IPv6 address in the
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
// by Tailscale.
func (b *LocalBackend) ShouldHandleViaIP(ip netip.Addr) bool {
@@ -3329,10 +3445,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
if len(p.Addresses) == 0 {
continue
}
if p.User != nm.User && b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharing) {
if !b.peerIsTaildropTargetLocked(p) {
continue
}
peerAPI := peerAPIBase(b.netMap, p)
@@ -3348,6 +3461,26 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
return ret, nil
}
// peerIsTaildropTargetLocked reports whether p is a valid Taildrop file
// recipient from this node according to its ownership and the capabilities in
// the netmap.
//
// b.mu must be locked.
func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool {
if b.netMap == nil || p == nil {
return false
}
if b.netMap.User == p.User {
return true
}
if len(p.Addresses) > 0 &&
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharingTarget) {
// Explicitly noted in the netmap ACL caps as a target.
return true
}
return false
}
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap string) bool {
for _, hasCap := range b.peerCapsLocked(addr) {
if hasCap == wantCap {
@@ -3603,7 +3736,7 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
return mc, nil
}
// DoNoiseRequest sends a request to URL over the the control plane
// DoNoiseRequest sends a request to URL over the control plane
// Noise connection.
func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
b.mu.Lock()
@@ -3684,3 +3817,19 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
}
io.WriteString(w, "</ul>\n")
}
func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
var checks []doctor.Check
checks = append(checks, routetable.Check{})
// TODO(andrew): more
numChecks := len(checks)
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
log("%d checks", numChecks)
return nil
}))
doctor.RunChecks(ctx, logf, checks...)
}

View File

@@ -18,7 +18,6 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
@@ -26,38 +25,82 @@ import (
"tailscale.com/types/tkatype"
)
var networkLockAvailable = envknob.RegisterBool("TS_EXPERIMENTAL_NETWORK_LOCK")
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
var (
errMissingNetmap = errors.New("missing netmap: verify that you are logged in")
errNetworkLockNotActive = errors.New("network-lock is not active")
)
type tkaState struct {
authority *tka.Authority
storage *tka.FS
}
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
// nodes from the netmap who's signature does not verify.
//
// b.mu must be held.
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
if !envknob.UseWIPCode() {
return // Feature-flag till network-lock is in Alpha.
}
if b.tka == nil {
return // TKA not enabled.
}
toDelete := make(map[int]struct{}, len(nm.Peers))
for i, p := range nm.Peers {
if len(p.KeySignature) == 0 {
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
toDelete[i] = struct{}{}
} else {
if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil {
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err)
toDelete[i] = struct{}{}
}
}
}
// nm.Peers is ordered, so deletion must be order-preserving.
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
for i, p := range nm.Peers {
if _, delete := toDelete[i]; !delete {
peers = append(peers, p)
}
}
nm.Peers = peers
}
// tkaSyncIfNeeded examines TKA info reported from the control plane,
// performing the steps necessary to synchronize local tka state.
//
// There are 4 scenarios handled here:
// - Enablement: nm.TKAEnabled but b.tka == nil
// ∴ reach out to /machine/tka/boostrap to get the genesis AUM, then
// ∴ reach out to /machine/tka/bootstrap to get the genesis AUM, then
// initialize TKA.
// - Disablement: !nm.TKAEnabled but b.tka != nil
// ∴ reach out to /machine/tka/boostrap to read the disablement secret,
// ∴ reach out to /machine/tka/bootstrap to read the disablement secret,
// then verify and clear tka local state.
// - Sync needed: b.tka.Head != nm.TKAHead
// ∴ complete multi-step synchronization flow.
// - Everything up to date: All other cases.
// ∴ no action necessary.
//
// b.mu must be held. b.mu will be stepped out of (and back in) during network
// RPCs.
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
if !networkLockAvailable() {
// tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
// and may take b.mu as required.
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
if !envknob.UseWIPCode() {
// If the feature flag is not enabled, pretend we don't exist.
return nil
}
if nm.SelfNode == nil {
return errors.New("SelfNode missing")
}
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
defer b.tkaSyncLock.Unlock()
b.mu.Lock() // take mu to protect access to synchronized fields.
defer b.mu.Unlock()
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
isEnabled := b.tka != nil
wantEnabled := nm.TKAEnabled
@@ -70,15 +113,15 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
// Regardless of whether we are moving to disabled or enabled, we
// need information from the tka bootstrap endpoint.
b.mu.Unlock()
bs, err := b.tkaFetchBootstrap(nm.SelfNode.ID, ourHead)
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
b.mu.Lock()
if err != nil {
return fmt.Errorf("fetching bootstrap: %v", err)
return fmt.Errorf("fetching bootstrap: %w", err)
}
if wantEnabled && !isEnabled {
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
return fmt.Errorf("bootstrap: %v", err)
return fmt.Errorf("bootstrap: %w", err)
}
isEnabled = true
} else if !wantEnabled && isEnabled {
@@ -98,7 +141,98 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
}
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
// TODO(tom): Implement sync
if err := b.tkaSyncLocked(ourNodeKey); err != nil {
return fmt.Errorf("tka sync: %w", err)
}
}
return nil
}
func toSyncOffer(head string, ancestors []string) (tka.SyncOffer, error) {
var out tka.SyncOffer
if err := out.Head.UnmarshalText([]byte(head)); err != nil {
return tka.SyncOffer{}, fmt.Errorf("head.UnmarshalText: %v", err)
}
out.Ancestors = make([]tka.AUMHash, len(ancestors))
for i, a := range ancestors {
if err := out.Ancestors[i].UnmarshalText([]byte(a)); err != nil {
return tka.SyncOffer{}, fmt.Errorf("ancestor[%d].UnmarshalText: %v", i, err)
}
}
return out, nil
}
// tkaSyncLocked synchronizes TKA state with control. b.mu must be held
// and tka must be initialized. b.mu will be stepped out of (and back into)
// during network RPCs.
//
// b.mu must be held.
func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
offer, err := b.tka.authority.SyncOffer(b.tka.storage)
if err != nil {
return fmt.Errorf("offer: %w", err)
}
b.mu.Unlock()
offerResp, err := b.tkaDoSyncOffer(ourNodeKey, offer)
b.mu.Lock()
if err != nil {
return fmt.Errorf("offer RPC: %w", err)
}
controlOffer, err := toSyncOffer(offerResp.Head, offerResp.Ancestors)
if err != nil {
return fmt.Errorf("control offer: %v", err)
}
if controlOffer.Head == offer.Head {
// We are up to date.
return nil
}
// Compute missing AUMs before we apply any AUMs from the control-plane,
// so we still submit AUMs to control even if they are not part of the
// active chain.
toSendAUMs, err := b.tka.authority.MissingAUMs(b.tka.storage, controlOffer)
if err != nil {
return fmt.Errorf("computing missing AUMs: %w", err)
}
// If we got this far, then we are not up to date. Either the control-plane
// has updates for us, or we have updates for the control plane.
//
// TODO(tom): Do we want to keep processing even if the Inform fails? Need
// to think through if theres holdback concerns here or not.
if len(offerResp.MissingAUMs) > 0 {
aums := make([]tka.AUM, len(offerResp.MissingAUMs))
for i, a := range offerResp.MissingAUMs {
if err := aums[i].Unserialize(a); err != nil {
return fmt.Errorf("MissingAUMs[%d]: %v", i, err)
}
}
if err := b.tka.authority.Inform(b.tka.storage, aums); err != nil {
return fmt.Errorf("inform failed: %v", err)
}
}
// NOTE(tom): We could short-circuit here if our HEAD equals the
// control-plane's head, but we don't just so control always has a
// copy of all forks that clients had.
b.mu.Unlock()
sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false)
b.mu.Lock()
if err != nil {
return fmt.Errorf("send RPC: %v", err)
}
var remoteHead tka.AUMHash
if err := remoteHead.UnmarshalText([]byte(sendResp.Head)); err != nil {
return fmt.Errorf("head unmarshal: %v", err)
}
if remoteHead != b.tka.authority.Head() {
b.logf("TKA desync: expected consensus after sync but our head is %v and the control plane's is %v", b.tka.authority.Head(), remoteHead)
}
return nil
@@ -115,8 +249,8 @@ func (b *LocalBackend) chonkPath() string {
//
// b.mu must be held.
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
if !b.CanSupportNetworkLock() {
return errors.New("network lock not supported in this configuration")
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
var genesis tka.AUM
@@ -145,26 +279,34 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
return nil
}
// CanSupportNetworkLock returns true if tailscaled is able to operate
// CanSupportNetworkLock returns nil if tailscaled is able to operate
// a local tailnet key authority (and hence enforce network lock).
func (b *LocalBackend) CanSupportNetworkLock() bool {
if b.tka != nil {
// The TKA is being used, so yeah its supported.
return true
func (b *LocalBackend) CanSupportNetworkLock() error {
if !envknob.UseWIPCode() {
return errors.New("this feature is not yet complete, a later release may support this functionality")
}
if b.TailscaleVarRoot() != "" {
// Theres a var root (aka --statedir), so if network lock gets
// initialized we have somewhere to store our AUMs. Thats all
// we need.
return true
if b.tka != nil {
// If the TKA is being used, it is supported.
return nil
}
return false
if b.TailscaleVarRoot() == "" {
return errors.New("network-lock is not supported in this configuration, try setting --statedir")
}
// There's a var root (aka --statedir), so if network lock gets
// initialized we have somewhere to store our AUMs. That's all
// we need.
return nil
}
// NetworkLockStatus returns a structure describing the state of the
// tailnet key authority, if any.
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return &ipnstate.NetworkLockStatus{
Enabled: false,
@@ -193,18 +335,18 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
// The Finish RPC submits signatures for all these nodes, at which point
// Control has everything it needs to atomically enable network lock.
func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
if b.tka != nil {
return errors.New("network-lock is already initialized")
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
if !networkLockAvailable() {
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
var ourNodeKey key.NodePublic
b.mu.Lock()
if b.prefs != nil {
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
}
if !b.CanSupportNetworkLock() {
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
}
nm := b.NetMap()
if nm == nil {
return errors.New("no netmap: are you logged into tailscale?")
b.mu.Unlock()
if ourNodeKey.IsZero() {
return errors.New("no node-key: is tailscale logged in?")
}
// Generates a genesis AUM representing trust in the provided keys.
@@ -226,7 +368,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
}
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
initResp, err := b.tkaInitBegin(nm, genesisAUM)
initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
if err != nil {
return fmt.Errorf("tka init-begin RPC: %w", err)
}
@@ -247,10 +389,91 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
}
// Finalize enablement by transmitting signature for all nodes to Control.
_, err = b.tkaInitFinish(nm, sigs)
_, err = b.tkaInitFinish(ourNodeKey, sigs)
return err
}
// Only use is in tests.
func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSignature, nodeKey key.NodePublic) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return errNetworkLockNotActive
}
return b.tka.authority.NodeKeyAuthorized(nodeKey, nks)
}
// Only use is in tests.
func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
panic("network lock not initialized")
}
return b.tka.authority.KeyTrusted(keyID)
}
// NetworkLockModify adds and/or removes keys in the tailnet's key authority.
func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("modify network-lock keys: %w", err)
}
}()
b.mu.Lock()
defer b.mu.Unlock()
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
if b.tka == nil {
return errNetworkLockNotActive
}
updater := b.tka.authority.NewUpdater(b.nlPrivKey)
for _, addKey := range addKeys {
if err := updater.AddKey(addKey); err != nil {
return err
}
}
for _, removeKey := range removeKeys {
if err := updater.RemoveKey(removeKey.ID()); err != nil {
return err
}
}
aums, err := updater.Finalize(b.tka.storage)
if err != nil {
return err
}
if len(aums) == 0 {
return nil
}
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
b.mu.Unlock()
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true)
b.mu.Lock()
if err != nil {
return err
}
var controlHead tka.AUMHash
if err := controlHead.UnmarshalText([]byte(resp.Head)); err != nil {
return err
}
lastHead := aums[len(aums)-1].Hash()
if controlHead != lastHead {
return errors.New("central tka head differs from submitted AUM, try again")
}
return nil
}
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
p, err := nodeInfo.NodePublic.MarshalBinary()
if err != nil {
@@ -270,10 +493,11 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
return &sig, nil
}
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
NodeID: nm.SelfNode.ID,
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
GenesisAUM: aum.Serialize(),
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
@@ -281,40 +505,34 @@ func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcf
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
bo := backoff.NewBackoff("tka-init-begin", b.logf, 5*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitBeginResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitBeginResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
NodeID: nm.SelfNode.ID,
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
Signatures: nks,
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
@@ -322,41 +540,36 @@ func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.Node
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
bo := backoff.NewBackoff("tka-init-finish", b.logf, 5*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("ctx: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitFinishResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKAInitFinishResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
// over noise. This is used to get values necessary to enable or disable TKA.
func (b *LocalBackend) tkaFetchBootstrap(nodeID tailcfg.NodeID, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
bootstrapReq := tailcfg.TKABootstrapRequest{
NodeID: nodeID,
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
}
if !head.IsZero() {
head, err := head.MarshalText()
@@ -398,3 +611,107 @@ func (b *LocalBackend) tkaFetchBootstrap(nodeID tailcfg.NodeID, head tka.AUMHash
return a, nil
}
func fromSyncOffer(offer tka.SyncOffer) (head string, ancestors []string, err error) {
headBytes, err := offer.Head.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("head.MarshalText: %v", err)
}
ancestors = make([]string, len(offer.Ancestors))
for i, ancestor := range offer.Ancestors {
hash, err := ancestor.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("ancestor[%d].MarshalText: %v", i, err)
}
ancestors[i] = string(hash)
}
return string(headBytes), ancestors, nil
}
// tkaDoSyncOffer sends a /machine/tka/sync/offer RPC to the control plane
// over noise. This is the first of two RPCs implementing tka synchronization.
func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncOffer) (*tailcfg.TKASyncOfferResponse, error) {
head, ancestors, err := fromSyncOffer(offer)
if err != nil {
return nil, fmt.Errorf("encoding offer: %v", err)
}
syncReq := tailcfg.TKASyncOfferRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
Head: head,
Ancestors: ancestors,
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(syncReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/offer", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKASyncOfferResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}
// tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
// over noise. This is the second of two RPCs implementing tka synchronization.
func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) {
sendReq := tailcfg.TKASyncSendRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
Interactive: interactive,
}
for i, a := range aums {
sendReq.MissingAUMs[i] = a.Serialize()
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(sendReq); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/send", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKASyncSendResponse)
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}

View File

@@ -15,12 +15,17 @@ import (
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/control/controlclient"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/tkatype"
)
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
@@ -47,8 +52,6 @@ func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
return cc
}
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
// httptest plumbing, despite the domain being unused in the actual noise request transport.
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
ts := httptest.NewUnstartedServer(handler)
ts.StartTLS()
@@ -61,7 +64,8 @@ func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server,
}
func TestTKAEnablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
nodePriv := key.NewNode()
// Make a fake TKA authority, getting a usable genesis AUM which
// our mock server can communicate.
@@ -83,8 +87,11 @@ func TestTKAEnablementFlow(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.NodeID != 420 {
t.Errorf("bootstrap nodeID=%v, want 420", body.NodeID)
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("bootstrap nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
}
if body.Head != "" {
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
@@ -98,6 +105,9 @@ func TestTKAEnablementFlow(t *testing.T) {
t.Fatal(err)
}
case "/machine/tka/sync/offer", "/machine/tka/sync/send":
t.Error("node attempted to sync, but should have been up to date")
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
@@ -112,15 +122,15 @@ func TestTKAEnablementFlow(t *testing.T) {
cc: cc,
ccAuto: cc,
logf: t.Logf,
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 420},
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: tka.AUMHash{},
TKAHead: a1.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -133,9 +143,10 @@ func TestTKAEnablementFlow(t *testing.T) {
}
func TestTKADisablementFlow(t *testing.T) {
networkLockAvailable = func() bool { return true } // Enable the feature flag
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -153,6 +164,7 @@ func TestTKADisablementFlow(t *testing.T) {
t.Fatalf("tka.Create() failed: %v", err)
}
returnWrongSecret := false
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
@@ -161,14 +173,11 @@ func TestTKADisablementFlow(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
var disablement []byte
switch body.NodeID {
case 42:
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
case 420:
disablement = disablementSecret
default:
t.Errorf("bootstrap nodeID=%v, wanted 42 or 420", body.NodeID)
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
}
var head tka.AUMHash
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
@@ -178,6 +187,13 @@ func TestTKADisablementFlow(t *testing.T) {
t.Errorf("reported head = %x, want %x", head, authority.Head())
}
var disablement []byte
if returnWrongSecret {
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
} else {
disablement = disablementSecret
}
w.WriteHeader(200)
out := tailcfg.TKABootstrapResponse{
DisablementSecret: disablement,
@@ -203,17 +219,17 @@ func TestTKADisablementFlow(t *testing.T) {
authority: authority,
storage: chonk,
},
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}
// Test that the wrong disablement secret does not shut down the authority.
// NodeID == 42 indicates this scenario to our mock server.
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 42},
returnWrongSecret = true
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -222,14 +238,11 @@ func TestTKADisablementFlow(t *testing.T) {
}
// Test the correct disablement secret shuts down the authority.
// NodeID == 420 indicates this scenario to our mock server.
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{ID: 420},
returnWrongSecret = false
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: false,
TKAHead: authority.Head(),
})
b.mu.Unlock()
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@@ -241,3 +254,285 @@ func TestTKADisablementFlow(t *testing.T) {
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
}
}
func TestTKASync(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
someKeyPriv := key.NewNLPrivate()
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
type tkaSyncScenario struct {
name string
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
// on control should be seeded with.
controlAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
// on the node should be seeded with.
nodeAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
}
tcs := []tkaSyncScenario{
{name: "up to date"},
{
name: "control has an update",
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.RemoveKey(someKey.ID()); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
{
// AKA 'control data loss' scenario
name: "node has an update",
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.RemoveKey(someKey.ID()); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
{
// AKA 'control data loss + update in the meantime' scenario
name: "node and control diverge",
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swiggity"}); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
b := a.NewUpdater(signer)
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swooty"}); err != nil {
t.Fatal(err)
}
aums, err := b.Finalize(storage)
if err != nil {
t.Fatal(err)
}
return aums
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
nlPriv := key.NewNLPrivate()
// Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
controlStorage := &tka.Mem{}
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
Keys: []tka.Key{key, someKey},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
if tc.controlAUMs != nil {
if err := controlAuthority.Inform(controlStorage, tc.controlAUMs(t, controlAuthority, controlStorage, nlPriv)); err != nil {
t.Fatalf("controlAuthority.Inform() failed: %v", err)
}
}
// Setup the TKA authority on the node.
nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
nodeAuthority, err := tka.Bootstrap(nodeStorage, bootstrap)
if err != nil {
t.Fatalf("tka.Bootstrap() failed: %v", err)
}
if tc.nodeAUMs != nil {
if err := nodeAuthority.Inform(nodeStorage, tc.nodeAUMs(t, nodeAuthority, nodeStorage, nlPriv)); err != nil {
t.Fatalf("nodeAuthority.Inform() failed: %v", err)
}
}
// Make a mock control server.
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/sync/offer":
body := new(tailcfg.TKASyncOfferRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
t.Logf("got sync offer:\n%+v", body)
nodeOffer, err := toSyncOffer(body.Head, body.Ancestors)
if err != nil {
t.Fatal(err)
}
controlOffer, err := controlAuthority.SyncOffer(controlStorage)
if err != nil {
t.Fatal(err)
}
sendAUMs, err := controlAuthority.MissingAUMs(controlStorage, nodeOffer)
if err != nil {
t.Fatal(err)
}
head, ancestors, err := fromSyncOffer(controlOffer)
if err != nil {
t.Fatal(err)
}
resp := tailcfg.TKASyncOfferResponse{
Head: head,
Ancestors: ancestors,
MissingAUMs: make([]tkatype.MarshaledAUM, len(sendAUMs)),
}
for i, a := range sendAUMs {
resp.MissingAUMs[i] = a.Serialize()
}
t.Logf("responding to sync offer with:\n%+v", resp)
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(resp); err != nil {
t.Fatal(err)
}
case "/machine/tka/sync/send":
body := new(tailcfg.TKASyncSendRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
t.Logf("got sync send:\n%+v", body)
toApply := make([]tka.AUM, len(body.MissingAUMs))
for i, a := range body.MissingAUMs {
if err := toApply[i].Unserialize(a); err != nil {
t.Fatalf("decoding missingAUM[%d]: %v", i, err)
}
}
if len(toApply) > 0 {
if err := controlAuthority.Inform(controlStorage, toApply); err != nil {
t.Fatalf("control.Inform(%+v) failed: %v", toApply, err)
}
}
head, err := controlAuthority.Head().MarshalText()
if err != nil {
t.Fatal(err)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{Head: string(head)}); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
// Setup the client.
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: nodeAuthority,
storage: nodeStorage,
},
prefs: &ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}
// Finally, lets trigger a sync.
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: controlAuthority.Head(),
})
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
// Check that at the end of this ordeal, the node and the control
// plane are in sync.
if nodeHead, controlHead := b.tka.authority.Head(), controlAuthority.Head(); nodeHead != controlHead {
t.Errorf("node head = %v, want %v", nodeHead, controlHead)
}
})
}
}
func TestTKAFilterNetmap(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
nlPriv := key.NewNLPrivate()
nlKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
storage := &tka.Mem{}
authority, _, err := tka.Create(storage, tka.State{
Keys: []tka.Key{nlKey},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
n4Sig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n4.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
n4Sig.Signature[3] = 42 // mess up the signature
n4Sig.Signature[4] = 42 // mess up the signature
n5GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n5.Public()}, nlPriv)
if err != nil {
t.Fatal(err)
}
nm := netmap.NetworkMap{
Peers: []*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
},
}
b := &LocalBackend{
logf: t.Logf,
tka: &tkaState{authority: authority},
}
b.tkaFilterNetmapLocked(&nm)
want := []*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
}
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
return x.Raw32() == y.Raw32()
})
if diff := cmp.Diff(nm.Peers, want, nodePubComparer); diff != "" {
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
}
}

View File

@@ -79,7 +79,7 @@ type peerAPIServer struct {
}
const (
// partialSuffix is the suffix appened to files while they're
// partialSuffix is the suffix appended to files while they're
// still in the process of being transferred.
partialSuffix = ".partial"
@@ -1184,7 +1184,7 @@ func newFakePeerAPIListener(ip netip.Addr) net.Listener {
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
// or we lack permission to listen on a port. It's okay to not actually listen via
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
// also intercept netstack TCP requests in to our peerapi port and hand it over
// also intercept incoming netstack TCP requests to our peerapi port and hand them over
// directly to peerapi, without involving the kernel. So this doesn't need to be
// real. But the port number we return (1, in this case) is the port number we advertise
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's

View File

@@ -109,7 +109,7 @@ func TestHandlePeerAPI(t *testing.T) {
tests := []struct {
name string
isSelf bool // the peer sending the request is owned by us
capSharing bool // self node has file sharing capabilty
capSharing bool // self node has file sharing capability
omitRoot bool // don't configure
req *http.Request
checks []check

View File

@@ -57,7 +57,7 @@ import (
// Options is the configuration of the Tailscale node agent.
type Options struct {
// VarRoot is the the Tailscale daemon's private writable
// VarRoot is the Tailscale daemon's private writable
// directory (usually "/var/lib/tailscale" on Linux) that
// contains the "tailscaled.state" file, the "certs" directory
// for TLS certs, and the "files" directory for incoming

View File

@@ -26,6 +26,8 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
@@ -35,9 +37,49 @@ import (
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/strs"
"tailscale.com/version"
)
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
// handler is the set of LocalAPI handlers, keyed by the part of the
// Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
// then it's a prefix match.
var handler = map[string]localAPIHandler{
// The prefix match handlers end with a slash:
"cert/": (*Handler).serveCert,
"file-put/": (*Handler).serveFilePut,
"files/": (*Handler).serveFiles,
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash:
"bugreport": (*Handler).serveBugReport,
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
"check-prefs": (*Handler).serveCheckPrefs,
"component-debug-logging": (*Handler).serveComponentDebugLogging,
"debug": (*Handler).serveDebug,
"derpmap": (*Handler).serveDERPMap,
"dial": (*Handler).serveDial,
"file-targets": (*Handler).serveFileTargets,
"goroutines": (*Handler).serveGoroutines,
"id-token": (*Handler).serveIDToken,
"login-interactive": (*Handler).serveLoginInteractive,
"logout": (*Handler).serveLogout,
"metrics": (*Handler).serveMetrics,
"ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs,
"profile": (*Handler).serveProfile,
"set-dns": (*Handler).serveSetDNS,
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
"status": (*Handler).serveStatus,
"tka/init": (*Handler).serveTKAInit,
"tka/modify": (*Handler).serveTKAModify,
"tka/status": (*Handler).serveTKAStatus,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"whois": (*Handler).serveWhoIs,
}
func randHex(n int) string {
b := make([]byte, n)
rand.Read(b)
@@ -99,68 +141,45 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}
if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
h.serveFiles(w, r)
return
if fn, ok := handlerForPath(r.URL.Path); ok {
fn(h, w, r)
} else {
http.NotFound(w, r)
}
if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") {
h.serveFilePut(w, r)
return
}
// handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
// (the path doesn't include any query parameters)
func handlerForPath(urlPath string) (h localAPIHandler, ok bool) {
if urlPath == "/" {
return (*Handler).serveLocalAPIRoot, true
}
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
h.serveCert(w, r)
return
suff, ok := strs.CutPrefix(urlPath, "/localapi/v0/")
if !ok {
// Currently all LocalAPI methods start with "/localapi/v0/" to signal
// to people that they're not necessarily stable APIs. In practice we'll
// probably need to keep them pretty stable anyway, but for now treat
// them as an internal implementation detail.
return nil, false
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
case "/localapi/v0/goroutines":
h.serveGoroutines(w, r)
case "/localapi/v0/profile":
h.serveProfile(w, r)
case "/localapi/v0/status":
h.serveStatus(w, r)
case "/localapi/v0/logout":
h.serveLogout(w, r)
case "/localapi/v0/login-interactive":
h.serveLoginInteractive(w, r)
case "/localapi/v0/prefs":
h.servePrefs(w, r)
case "/localapi/v0/ping":
h.servePing(w, r)
case "/localapi/v0/check-prefs":
h.serveCheckPrefs(w, r)
case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r)
case "/localapi/v0/bugreport":
h.serveBugReport(w, r)
case "/localapi/v0/file-targets":
h.serveFileTargets(w, r)
case "/localapi/v0/set-dns":
h.serveSetDNS(w, r)
case "/localapi/v0/derpmap":
h.serveDERPMap(w, r)
case "/localapi/v0/metrics":
h.serveMetrics(w, r)
case "/localapi/v0/debug":
h.serveDebug(w, r)
case "/localapi/v0/set-expiry-sooner":
h.serveSetExpirySooner(w, r)
case "/localapi/v0/dial":
h.serveDial(w, r)
case "/localapi/v0/id-token":
h.serveIDToken(w, r)
case "/localapi/v0/upload-client-metrics":
h.serveUploadClientMetrics(w, r)
case "/localapi/v0/tka/status":
h.serveTkaStatus(w, r)
case "/localapi/v0/tka/init":
h.serveTkaInit(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
http.Error(w, "404 not found", 404)
if fn, ok := handler[suff]; ok {
// Here we match exact handler suffixes like "status" or ones with a
// slash already in their name, like "tka/status".
return fn, true
}
// Otherwise, it might be a prefix match like "files/*" which we look up
// by the prefix including first trailing slash.
if i := strings.IndexByte(suff, '/'); i != -1 {
suff = suff[:i+1]
if fn, ok := handler[suff]; ok {
return fn, true
}
}
return nil, false
}
func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "tailscaled\n")
}
// serveIDToken handles requests to get an OIDC ID token.
@@ -213,16 +232,81 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
return
}
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
if envknob.NoLogsNoSupport() {
logMarker = "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled"
logMarker := func() string {
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
}
h.logf("user bugreport: %s", logMarker)
if note := r.FormValue("note"); len(note) > 0 {
if envknob.NoLogsNoSupport() {
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
}
startMarker := logMarker()
h.logf("user bugreport: %s", startMarker)
if note := r.URL.Query().Get("note"); len(note) > 0 {
h.logf("user bugreport note: %s", note)
}
hi, _ := json.Marshal(hostinfo.New())
h.logf("user bugreport hostinfo: %s", hi)
if err := health.OverallError(); err != nil {
h.logf("user bugreport health: %s", err.Error())
} else {
h.logf("user bugreport health: ok")
}
if defBool(r.URL.Query().Get("diagnose"), false) {
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, logMarker)
fmt.Fprintln(w, startMarker)
// Nothing else to do if we're not in record mode; we wrote the marker
// above, so we can just finish our response now.
if !defBool(r.URL.Query().Get("record"), false) {
return
}
until := time.Now().Add(12 * time.Hour)
var changed map[string]bool
for _, component := range []string{"magicsock"} {
if h.b.GetComponentDebugLogging(component).IsZero() {
if err := h.b.SetComponentDebugLogging(component, until); err != nil {
h.logf("bugreport: error setting component %q logging: %v", component, err)
continue
}
mak.Set(&changed, component, true)
}
}
defer func() {
for component := range changed {
h.b.SetComponentDebugLogging(component, time.Time{})
}
}()
// NOTE(andrew): if we have anything else we want to do while recording
// a bugreport, we can add it here.
// Read from the client; this will also return when the client closes
// the connection.
var buf [1]byte
_, err := r.Body.Read(buf[:])
switch {
case err == nil:
// good
case errors.Is(err, io.EOF):
// good
case errors.Is(err, io.ErrUnexpectedEOF):
// this happens when Ctrl-C'ing the tailscale client; don't
// bother logging an error
default:
// Log but continue anyway.
h.logf("user bugreport: error reading body: %v", err)
}
// Generate another log marker and return it to the client.
endMarker := logMarker()
h.logf("user bugreport end: %s", endMarker)
fmt.Fprintln(w, endMarker)
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
@@ -315,6 +399,24 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "done\n")
}
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
err := h.b.SetComponentDebugLogging(component, time.Now().Add(time.Duration(secs)*time.Second))
var res struct {
Error string
}
if err != nil {
res.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)
@@ -800,13 +902,13 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
json.NewEncoder(w).Encode(struct{}{})
}
func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "lock status access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodGet {
http.Error(w, "use Get", http.StatusMethodNotAllowed)
http.Error(w, "use GET", http.StatusMethodNotAllowed)
return
}
@@ -819,7 +921,7 @@ func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "lock init access denied", http.StatusForbidden)
return
@@ -852,6 +954,40 @@ func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
var req modifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", 400)
return
}
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
return
}
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
func defBool(a string, def bool) bool {
if a == "" {
return def

View File

@@ -470,7 +470,7 @@ func TestLoadPrefsNotExist(t *testing.T) {
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
}
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs handles corrupted input files.
// See issue #954 for details.
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")

View File

@@ -6,6 +6,8 @@ package ipn
import (
"errors"
"fmt"
"strconv"
)
// ErrStateNotExist is returned by StateStore.ReadState when the
@@ -35,7 +37,7 @@ const (
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
// NLKeyStateKey is the key under which we store the nodes'
// NLKeyStateKey is the key under which we store the node's
// network-lock node key, in its key.NLPrivate.MarshalText representation.
NLKeyStateKey = StateKey("_nl-node-key")
)
@@ -48,3 +50,17 @@ type StateStore interface {
// WriteState saves bs as the state associated with ID.
WriteState(id StateKey, bs []byte) error
}
// ReadStoreInt reads an integer from a StateStore.
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
v, err := store.ReadState(id)
if err != nil {
return 0, err
}
return strconv.ParseInt(string(v), 10, 64)
}
// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
return store.WriteState(id, fmt.Appendf(nil, "%d", val))
}

View File

@@ -100,7 +100,9 @@ func (c *Client) secretURL(name string) string {
}
func getError(resp *http.Response) error {
if resp.StatusCode == 200 {
if resp.StatusCode == 200 || resp.StatusCode == 201 {
// These are the only success codes returned by the Kubernetes API.
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#http-status-codes
return nil
}
st := &Status{}

View File

@@ -38,7 +38,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/777337dba4cf/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
@@ -55,9 +55,9 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/807a2327:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/a66eb644:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841: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/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc: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/+/f0f3c7e8:LICENSE))

View File

@@ -27,7 +27,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/777337dba4cf/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
@@ -39,9 +39,9 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0: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/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841: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/+/886fb937:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc: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/+/f0f3c7e8:LICENSE))
- [golang.zx2c4.com/wireguard](https://pkg.go.dev/golang.zx2c4.com/wireguard) ([MIT](https://git.zx2c4.com/wireguard-go/tree/LICENSE?id=b51010ba13f0))

View File

@@ -58,7 +58,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.4/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/62f465106986/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
@@ -71,9 +71,9 @@ Some packages may only be included on certain architectures or operating systems
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/eb4f295c:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/a9213eeb:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841: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/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc: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/+/f0f3c7e8:LICENSE))

View File

@@ -31,9 +31,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [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/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0: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/+/0de741cf:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841: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/+/886fb937:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))

View File

@@ -70,11 +70,24 @@ func getLogTarget() string {
return getLogTargetOnce.v
}
// LogURL is the base URL for the configured logtail server, or the default.
// It is guaranteed to not terminate with any forward slashes.
func LogURL() string {
if v := getLogTarget(); v != "" {
return strings.TrimRight(v, "/")
}
return "https://" + logtail.DefaultHost
}
// LogHost returns the hostname only (without port) of the configured
// logtail server, or the default.
//
// Deprecated: Use LogURL instead.
func LogHost() string {
if v := getLogTarget(); v != "" {
return v
if u, err := url.Parse(v); err == nil {
return u.Hostname()
}
}
return logtail.DefaultHost
}
@@ -596,7 +609,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy {
}
}
log.SetFlags(0) // other logflags are set on console, not here
log.SetFlags(0) // other log flags are set on console, not here
log.SetOutput(logOutput)
log.Printf("Program starting: v%v, Go %v: %#v",

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logpolicy
import (
"os"
"reflect"
"testing"
)
func TestLogHost(t *testing.T) {
v := reflect.ValueOf(&getLogTargetOnce).Elem()
reset := func() {
v.Set(reflect.Zero(v.Type()))
}
defer reset()
tests := []struct {
env string
want string
}{
{"", "log.tailscale.io"},
{"http://foo.com", "foo.com"},
{"https://foo.com", "foo.com"},
{"https://foo.com/", "foo.com"},
{"https://foo.com:123/", "foo.com"},
}
for _, tt := range tests {
reset()
os.Setenv("TS_LOG_TARGET", tt.env)
if got := LogHost(); got != tt.want {
t.Errorf("for env %q, got %q, want %q", tt.env, got, tt.want)
}
}
}

View File

@@ -34,7 +34,7 @@ func NewPrivateID() (id PrivateID, err error) {
func (id PrivateID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PrivateID.MarhsalText: i=%d", i)
return nil, fmt.Errorf("logtail.PrivateID.MarshalText: i=%d", i)
}
return b, nil
}
@@ -122,7 +122,7 @@ func MustParsePublicID(s string) PublicID {
func (id PublicID) MarshalText() ([]byte, error) {
b := make([]byte, hex.EncodedLen(len(id)))
if i := hex.Encode(b, id[:]); i != len(b) {
return nil, fmt.Errorf("logtail.PublicID.MarhsalText: i=%d", i)
return nil, fmt.Errorf("logtail.PublicID.MarshalText: i=%d", i)
}
return b, nil
}

View File

@@ -44,12 +44,13 @@ type Encoder interface {
type Config struct {
Collection string // collection name, a domain name
PrivateID PrivateID // machine-specific private identifier
PrivateID PrivateID // private ID for the primary log stream
CopyPrivateID PrivateID // private ID for a log stream that is a superset of this log stream
BaseURL string // if empty defaults to "https://log.tailscale.io"
HTTPC *http.Client // if empty defaults to http.DefaultClient
SkipClientTime bool // if true, client_time is not written to logs
LowMemory bool // if true, logtail minimizes memory use
TimeNow func() time.Time // if set, subsitutes uses of time.Now
TimeNow func() time.Time // if set, substitutes uses of time.Now
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
Buffer Buffer // temp storage, if nil a MemoryBuffer
@@ -73,7 +74,7 @@ type Config struct {
// IncludeProcSequence, if true, results in an ephemeral sequence number
// being included in the logs. The sequence number is incremented for each
// log message sent, but is not peristed across process restarts.
// log message sent, but is not persisted across process restarts.
IncludeProcSequence bool
}
@@ -112,12 +113,16 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
stdLogf := func(f string, a ...any) {
fmt.Fprintf(cfg.Stderr, strings.TrimSuffix(f, "\n")+"\n", a...)
}
var urlSuffix string
if !cfg.CopyPrivateID.IsZero() {
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
}
l := &Logger{
privateID: cfg.PrivateID,
stderr: cfg.Stderr,
stderrLevel: int64(cfg.StderrLevel),
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String() + urlSuffix,
lowMem: cfg.LowMemory,
buffer: cfg.Buffer,
skipClientTime: cfg.SkipClientTime,
@@ -519,7 +524,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, proc
b = append(b, `"logtail": {`...)
if !skipClientTime {
b = append(b, `"client_time": "`...)
b = now.AppendFormat(b, time.RFC3339Nano)
b = now.UTC().AppendFormat(b, time.RFC3339Nano)
b = append(b, `",`...)
}
if procID != 0 {
@@ -612,7 +617,7 @@ func (l *Logger) encodeLocked(buf []byte, level int) []byte {
if !l.skipClientTime || l.procID != 0 || l.procSequence != 0 {
logtail := map[string]any{}
if !l.skipClientTime {
logtail["client_time"] = now.Format(time.RFC3339Nano)
logtail["client_time"] = now.UTC().Format(time.RFC3339Nano)
}
if l.procID != 0 {
logtail["proc_id"] = l.procID

View File

@@ -381,7 +381,7 @@ func (m *Manager) NextPacket() ([]byte, error) {
return buf, nil
}
// Query executes a DNS query recieved from the given address. The query is
// Query executes a DNS query received from the given address. The query is
// provided in bs as a wire-encoded DNS query without any transport header.
// This method is called for requests arriving over UDP and TCP.
func (m *Manager) Query(ctx context.Context, bs []byte, from netip.AddrPort) ([]byte, error) {
@@ -540,7 +540,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
logf("creating dns cleanup: %v", err)
return
}
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
dns := NewManager(logf, oscfg, nil, &tsdial.Dialer{Logf: logf}, nil)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}

View File

@@ -274,7 +274,7 @@ func runTest(t *testing.T, isLocal bool) {
runCase := func(n int) {
t.Logf("Test case: %d domains\n", n)
if !isLocal {
// When !isLocal, we want to check that a GP notification occured for
// When !isLocal, we want to check that a GP notification occurred for
// every single test case.
trk, err = newGPNotificationTracker()
if err != nil {

View File

@@ -302,7 +302,7 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
for _, cfg := range cfgs {
if name, ok := cfg["interface"]; ok {
if s, ok := name.Value().(string); ok && s == m.interfaceName {
// Config for the taislcale interface, skip.
// Config for the tailscale interface, skip.
continue
}
}

View File

@@ -58,7 +58,7 @@ var (
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
// nrptRuleDatabase ensapsulates access to the Windows Name Resolution Policy
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
// Table (NRPT).
type nrptRuleDatabase struct {
logf logger.Logf

View File

@@ -5,9 +5,12 @@
package dns
import (
"bufio"
"errors"
"fmt"
"net/netip"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -97,6 +100,44 @@ func (a OSConfig) Equal(b OSConfig) bool {
return true
}
// Format implements the fmt.Formatter interface to ensure that Hosts is
// printed correctly (i.e. not as a bunch of pointers).
//
// Fixes https://github.com/tailscale/tailscale/issues/5669
func (a OSConfig) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
w.WriteString(`{Nameservers:[`)
for i, ns := range a.Nameservers {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", ns)
}
w.WriteString(`] SearchDomains:[`)
for i, domain := range a.SearchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] MatchDomains:[`)
for i, domain := range a.MatchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] Hosts:[`)
for i, host := range a.Hosts {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", host)
}
w.WriteString(`]}`)
}).Format(f, verb)
}
// ErrGetBaseConfigNotSupported is the error
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.

44
net/dns/osconfig_test.go Normal file
View File

@@ -0,0 +1,44 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dns
import (
"fmt"
"net/netip"
"testing"
"tailscale.com/util/dnsname"
)
func TestOSConfigPrintable(t *testing.T) {
ocfg := OSConfig{
Hosts: []*HostEntry{
{
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}),
Hosts: []string{"server", "client"},
},
{
Addr: netip.AddrFrom4([4]byte{100, 1, 2, 4}),
Hosts: []string{"otherhost"},
},
},
Nameservers: []netip.Addr{
netip.AddrFrom4([4]byte{8, 8, 8, 8}),
},
SearchDomains: []dnsname.FQDN{
dnsname.FQDN("foo.beta.tailscale.net."),
dnsname.FQDN("bar.beta.tailscale.net."),
},
MatchDomains: []dnsname.FQDN{
dnsname.FQDN("ts.com."),
},
}
s := fmt.Sprintf("%+v", ocfg)
const expected = `{Nameservers:[8.8.8.8] SearchDomains:[foo.beta.tailscale.net. bar.beta.tailscale.net.] MatchDomains:[ts.com.] Hosts:[&{Addr:100.1.2.3 Hosts:[server client]} &{Addr:100.1.2.4 Hosts:[otherhost]}]}`
if s != expected {
t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected)
}
}

View File

@@ -37,15 +37,15 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
}
// NextDNS DoH URLs are of the form "https://dns.nextdns.io/c3a884"
// where the path component is the lower 8 bytes of the IPv6 address
// where the path component is the lower 12 bytes of the IPv6 address
// in lowercase hex without any zero padding.
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
a := ip.As16()
var sb strings.Builder
const base = "https://dns.nextdns.io/"
sb.Grow(len(base) + 8)
sb.Grow(len(base) + 12)
sb.WriteString(base)
for _, b := range bytes.TrimLeft(a[8:], "\x00") {
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
fmt.Fprintf(&sb, "%02x", b)
}
return sb.String(), true, true
@@ -100,7 +100,7 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
// conventional for them and not required (it'll already be in the DoH path).
// (Really we shouldn't use either IPv4 or IPv6 anycast for DoH once we
// resolve "dns.nextdns.io".)
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 8 && len(b) > 0 {
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 12 && len(b) > 0 {
return []netip.Addr{
nextDNSv4One,
nextDNSv4Two,
@@ -215,7 +215,7 @@ var (
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
// provided ip and using id as the lowest 0-8 bytes.
func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
if len(id) > 8 {
if len(id) > 12 {
return netip.Addr{}
}
a := ip.As16()

View File

@@ -86,6 +86,19 @@ func TestDoHIPsOfBase(t *testing.T) {
"2a07:a8c1::c3:a884",
),
},
{
base: "https://dns.nextdns.io/112233445566778899aabbcc",
want: ips(
"45.90.28.0",
"45.90.30.0",
"2a07:a8c0:1122:3344:5566:7788:99aa:bbcc",
"2a07:a8c1:1122:3344:5566:7788:99aa:bbcc",
),
},
{
base: "https://dns.nextdns.io/112233445566778899aabbccdd",
want: ips(), // nothing; profile length is over 12 bytes
},
{
base: "https://dns.nextdns.io/c3a884/with/more/stuff",
want: ips(

View File

@@ -180,7 +180,7 @@ type resolverAndDelay struct {
type forwarder struct {
logf logger.Logf
linkMon *monitor.Mon
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absorbs it
dialer *tsdial.Dialer
dohSem chan struct{}
@@ -502,7 +502,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
// Only known DoH providers are supported currently. Specifically, we
// only support DoH providers where we can TCP connect to them on port
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custom DoH providers
// aren't currently supported. There's no backup DNS resolution path for
// them.
urlBase := rr.name.Addr

View File

@@ -609,7 +609,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
metricDNSResolveLocalOKAll.Add(1)
return addrs[0], dns.RCodeSuccess
// Leave some some record types explicitly unimplemented.
// Leave some record types explicitly unimplemented.
// These types relate to recursive resolution or special
// DNS semantics and might be implemented in the future.
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:

View File

@@ -99,7 +99,7 @@ type msgResource struct {
}
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
// when the request can not be satisified from cache.
// when the request can not be satisfied from cache.
var ErrCacheMiss = errors.New("cache miss")
var parserPool = &sync.Pool{
@@ -264,7 +264,7 @@ func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
}
// packDNSResponse builds a DNS response for the given question and
// transaction ID. The response resource records will have have the
// transaction ID. The response resource records will have the
// same provided TTL.
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
var baseMem []byte // TODO: guess a max size based on looping over answers?

View File

@@ -20,9 +20,9 @@ import (
// Tuple is a 5-tuple of proto, source and destination IP and port.
type Tuple struct {
Proto ipproto.Proto
Src netip.AddrPort
Dst netip.AddrPort
Proto ipproto.Proto `json:"proto"`
Src netip.AddrPort `json:"src"`
Dst netip.AddrPort `json:"dst"`
}
func (t Tuple) String() string {

View File

@@ -441,13 +441,13 @@ func prefixesEqual(a, b []netip.Prefix) bool {
// UseInterestingInterfaces is an InterfaceFilter that reports whether i is an interesting interface.
// An interesting interface if it is (a) not owned by Tailscale and (b) routes interesting IP addresses.
// See UseInterestingIPs for the defition of an interesting IP address.
// See UseInterestingIPs for the definition of an interesting IP address.
func UseInterestingInterfaces(i Interface, ips []netip.Prefix) bool {
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
}
// UseInterestingIPs is an IPFilter that reports whether ip is an interesting IP address.
// An IP address is interesting if it is neither a lopback not a link local unicast IP address.
// An IP address is interesting if it is neither a loopback nor a link local unicast IP address.
func UseInterestingIPs(ip netip.Addr) bool {
return isInterestingIP(ip)
}
@@ -455,7 +455,7 @@ func UseInterestingIPs(ip netip.Addr) bool {
// UseAllInterfaces is an InterfaceFilter that includes all interfaces.
func UseAllInterfaces(i Interface, ips []netip.Prefix) bool { return true }
// UseAllIPs is an IPFilter that includes all all IPs.
// UseAllIPs is an IPFilter that includes all IPs.
func UseAllIPs(ips netip.Addr) bool { return true }
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }

View File

@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This might work on other BSDs, but only tested on FreeBSD.
// Originally a fork of interfaces_darwin.go with slightly different flags.
// Common code for FreeBSD and Darwin. This might also work on other
// BSD systems (e.g. OpenBSD) but has not been tested.
//go:build freebsd
// +build freebsd
//go:build darwin || freebsd
// +build darwin freebsd
package interfaces
@@ -37,11 +37,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
@@ -61,35 +56,20 @@ func DefaultRouteInterfaceIndex() (int, error) {
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
msgs, err := parseRoutingTable(rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
if isDefaultGateway(rm) {
return rm.Index, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
return 0, errors.New("no gateway index found")
}
func init() {
@@ -102,7 +82,7 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
log.Printf("routerIP/FetchRIB: %v", err)
return ret, false
}
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
msgs, err := parseRoutingTable(rib)
if err != nil {
log.Printf("routerIP/ParseRIB: %v", err)
return ret, false
@@ -112,26 +92,54 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
if !ok {
continue
}
const RTF_IFSCOPE = 0x1000000
if rm.Flags&unix.RTF_GATEWAY == 0 {
if !isDefaultGateway(rm) {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
if len(rm.Addrs) > unix.RTAX_GATEWAY {
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
// Expect 0.0.0.0 as DST field.
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
return ret, false
}
var v4default = [4]byte{0, 0, 0, 0}
var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
func isDefaultGateway(rm *route.RouteMessage) bool {
if rm.Flags&unix.RTF_GATEWAY == 0 {
return false
}
// Defined locally because FreeBSD does not have unix.RTF_IFSCOPE.
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_IFSCOPE != 0 {
return false
}
// Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...]
if len(rm.Addrs) <= unix.RTAX_NETMASK {
return false
}
dst := rm.Addrs[unix.RTAX_DST]
netmask := rm.Addrs[unix.RTAX_NETMASK]
if dst.Family() == syscall.AF_INET &&
netmask.Family() == syscall.AF_INET &&
dst.(*route.Inet4Addr).IP == v4default &&
netmask.(*route.Inet4Addr).IP == v4default {
return true
}
if dst.Family() == syscall.AF_INET6 &&
netmask.Family() == syscall.AF_INET6 &&
dst.(*route.Inet6Addr).IP == v6default &&
netmask.(*route.Inet6Addr).IP == v6default {
return true
}
return false
}

View File

@@ -5,128 +5,16 @@
package interfaces
import (
"errors"
"fmt"
"log"
"net"
"net/netip"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
)
func defaultRoute() (d DefaultRouteDetails, err error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return d, err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return d, err
}
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
// Internet:
// Destination Gateway Flags Netif Expire
// default 10.0.0.1 UGSc en0 <-- want this one
// default 10.0.0.1 UGScI en1
// From man netstat:
// U RTF_UP Route usable
// G RTF_GATEWAY Destination requires forwarding by intermediary
// S RTF_STATIC Manually added
// c RTF_PRCLONING Protocol-specified generate new routes on use
// I RTF_IFSCOPE Route is associated with an interface scope
rib, err := fetchRoutingTable()
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}
func init() {
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
}
func likelyHomeRouterIPDarwinFetchRIB() (ret netip.Addr, ok bool) {
rib, err := fetchRoutingTable()
if err != nil {
log.Printf("routerIP/FetchRIB: %v", err)
return ret, false
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
log.Printf("routerIP/ParseRIB: %v", err)
return ret, false
}
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
if len(rm.Addrs) > unix.RTAX_GATEWAY {
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
// Expect 0.0.0.0 as DST field.
continue
}
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
if !ok {
continue
}
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
}
}
return ret, false
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
}

View File

@@ -16,18 +16,32 @@ import (
)
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
syscallIP, syscallOK := likelyHomeRouterIPBSDFetchRIB()
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
if syscallOK != netstatOK || syscallIP != netstatIP {
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
syscallIP, syscallOK,
netstatIP, netstatOK,
)
}
if !syscallOK {
return
}
def, err := defaultRoute()
if err != nil {
t.Errorf("defaultRoute() error: %v", err)
}
if def.InterfaceName != netstatIf {
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
}
}
/*
Parse out 10.0.0.1 from:
Parse out 10.0.0.1 and en0 from:
$ netstat -r -n -f inet
Routing tables
@@ -40,12 +54,12 @@ default link#14 UCSI utun2
10.0.0.1/32 link#4 UCS en0 !
...
*/
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
if version.IsMobile() {
// Don't try to do subprocesses on iOS. Ends up with log spam like:
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
// This is why we have likelyHomeRouterIPDarwinSyscall.
return ret, false
return ret, "", false
}
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
stdout, err := cmd.StdoutPipe()
@@ -64,22 +78,26 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
return nil
}
f = mem.AppendFields(f[:0], line)
if len(f) < 3 || !f[0].EqualString("default") {
if len(f) < 4 || !f[0].EqualString("default") {
return nil
}
ipm, flagsm := f[1], f[2]
ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) {
return nil
}
if mem.Contains(flagsm, mem.S("I")) {
return nil
}
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() {
ret = ip
netif = netifm.StringCopy()
// We've found what we're looking for.
return errStopReadingNetstatTable
}
return nil
})
return ret, ret.IsValid()
return ret, netif, ret.IsValid()
}
func TestFetchRoutingTable(t *testing.T) {

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This might work on other BSDs, but only tested on FreeBSD.
//go:build freebsd
// +build freebsd
package interfaces
import (
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
func fetchRoutingTable() (rib []byte, err error) {
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
}
func parseRoutingTable(rib []byte) ([]route.Message, error) {
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
}

View File

@@ -161,7 +161,7 @@ type Client struct {
// GetSTUNConn4 optionally provides a func to return the
// connection to use for sending & receiving IPv4 packets. If
// nil, an emphemeral one is created as needed.
// nil, an ephemeral one is created as needed.
GetSTUNConn4 func() STUNConn
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
@@ -1105,6 +1105,9 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
}
rids = append(rids, id)
}
if len(rids) == 0 {
return false, nil
}
preferredDERP = rids[rand.Intn(len(rids))]
}
@@ -1113,13 +1116,20 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
if err != nil {
return false, err
}
chal := "tailscale " + node.HostName
req.Header.Set("X-Tailscale-Challenge", chal)
r, err := noRedirectClient.Do(req)
if err != nil {
return false, err
}
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
defer r.Body.Close()
return r.StatusCode != 204, nil
expectedResponse := "response " + chal
validResponse := r.Header.Get("X-Tailscale-Response") == expectedResponse
c.logf("[v2] checkCaptivePortal url=%q status_code=%d valid_response=%v", req.URL.String(), r.StatusCode, validResponse)
return r.StatusCode != 204 || !validResponse, nil
}
// runHTTPOnlyChecks is the netcheck done by environments that can
@@ -1188,6 +1198,8 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
var ip netip.Addr
dc := derphttp.NewNetcheckClient(c.logf)
defer dc.Close()
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
if err != nil {
return 0, ip, err

View File

@@ -6,6 +6,7 @@ package nettest
import (
"net"
"net/netip"
"time"
)
@@ -32,20 +33,38 @@ func NewConn(name string, maxBuf int) (Conn, Conn) {
return &connHalf{r: r, w: w}, &connHalf{r: w, w: r}
}
// NewTCPConn creates a pair of Conns that are wired together by pipes.
func NewTCPConn(src, dst netip.AddrPort, maxBuf int) (local Conn, remote Conn) {
r := NewPipe(src.String(), maxBuf)
w := NewPipe(dst.String(), maxBuf)
lAddr := net.TCPAddrFromAddrPort(src)
rAddr := net.TCPAddrFromAddrPort(dst)
return &connHalf{r: r, w: w, remote: rAddr, local: lAddr}, &connHalf{r: w, w: r, remote: lAddr, local: rAddr}
}
type connAddr string
func (a connAddr) Network() string { return "mem" }
func (a connAddr) String() string { return string(a) }
type connHalf struct {
r, w *Pipe
local, remote net.Addr
r, w *Pipe
}
func (c *connHalf) LocalAddr() net.Addr {
if c.local != nil {
return c.local
}
return connAddr(c.r.name)
}
func (c *connHalf) RemoteAddr() net.Addr {
if c.remote != nil {
return c.remote
}
return connAddr(c.w.name)
}

View File

@@ -15,7 +15,7 @@ const (
bufferSize = 256 * 1024
)
// Listener is a net.Listener using using NewConn to create pairs of network
// Listener is a net.Listener using NewConn to create pairs of network
// connections connected in memory using a buffered pipe. It also provides a
// Dial method to establish new connections.
type Listener struct {
@@ -61,7 +61,7 @@ func (l *Listener) Accept() (net.Conn, error) {
// The provided Context must be non-nil. If the context expires before the
// connection is complete, an error is returned. Once successfully connected
// any expiration of the context will not affect the connection.
func (l *Listener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn, err error) {
if !strings.HasSuffix(network, "tcp") {
return nil, net.UnknownNetworkError(network)
}
@@ -72,6 +72,13 @@ func (l *Listener) Dial(ctx context.Context, network, addr string) (net.Conn, er
}
}
c, s := NewConn(addr, bufferSize)
defer func() {
if err != nil {
c.Close()
s.Close()
}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()

View File

@@ -195,7 +195,7 @@ const (
// given interface.
// The iface param determines which interface to check against, "" means to check
// global config.
// It tries to lookup the value directly from `/proc/sys`, and fallsback to
// It tries to lookup the value directly from `/proc/sys`, and falls back to
// using `sysctl` on failure.
func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
k := ipForwardSysctlKey(slashFormat, p, iface)

View File

@@ -40,7 +40,7 @@ type Header interface {
}
// HeaderChecksummer is implemented by Header implementations that
// need to do a checksum over their paylods.
// need to do a checksum over their payloads.
type HeaderChecksummer interface {
Header

View File

@@ -202,7 +202,7 @@ func TestPingerMismatch(t *testing.T) {
func mockPinger(t *testing.T, clock *tstest.Clock) (*Pinger, func()) {
// In tests, we use UDP so that we can test without being root; this
// doesn't matter becuase we mock out the ICMP reply below to be a real
// doesn't matter because we mock out the ICMP reply below to be a real
// ICMP echo reply packet.
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {

View File

@@ -19,7 +19,7 @@ import (
"tailscale.com/types/logger"
)
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
// TestIGD is an IGD (Internet Gateway Device) for testing. It supports fake
// implementations of NAT-PMP, PCP, and/or UPnP to test clients against.
type TestIGD struct {
upnpConn net.PacketConn // for UPnP discovery

View File

@@ -0,0 +1,151 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package routetable provides functions that operate on the system's route
// table.
package routetable
import (
"bufio"
"fmt"
"net/netip"
"strconv"
"tailscale.com/types/logger"
)
var (
defaultRouteIPv4 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
defaultRouteIPv6 = RouteDestination{Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
)
// RouteEntry contains common cross-platform fields describing an entry in the
// system route table.
type RouteEntry struct {
// Family is the IP family of the route; it will be either 4 or 6.
Family int
// Type is the type of this route.
Type RouteType
// Dst is the destination of the route.
Dst RouteDestination
// Gatewayis the gateway address specified for this route.
// This value will be invalid (where !r.Gateway.IsValid()) in cases
// where there is no gateway address for this route.
Gateway netip.Addr
// Interface is the name of the network interface to use when sending
// packets that match this route. This field can be empty.
Interface string
// Sys contains platform-specific information about this route.
Sys any
}
// Format implements the fmt.Formatter interface.
func (r RouteEntry) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
switch r.Family {
case 4:
fmt.Fprintf(w, "{Family: IPv4")
case 6:
fmt.Fprintf(w, "{Family: IPv6")
default:
fmt.Fprintf(w, "{Family: unknown(%d)", r.Family)
}
// Match 'ip route' and other tools by not printing the route
// type if it's a unicast route.
if r.Type != RouteTypeUnicast {
fmt.Fprintf(w, ", Type: %s", r.Type)
}
if r.Dst.IsValid() {
fmt.Fprintf(w, ", Dst: %s", r.Dst)
} else {
w.WriteString(", Dst: invalid")
}
if r.Gateway.IsValid() {
fmt.Fprintf(w, ", Gateway: %s", r.Gateway)
}
if r.Interface != "" {
fmt.Fprintf(w, ", Interface: %s", r.Interface)
}
if r.Sys != nil {
var formatVerb string
switch {
case f.Flag('#'):
formatVerb = "%#v"
case f.Flag('+'):
formatVerb = "%+v"
default:
formatVerb = "%v"
}
fmt.Fprintf(w, ", Sys: "+formatVerb, r.Sys)
}
w.WriteString("}")
}).Format(f, verb)
}
// RouteDestination is the destination of a route.
//
// This is similar to net/netip.Prefix, but also contains an optional IPv6
// zone.
type RouteDestination struct {
netip.Prefix
Zone string
}
func (r RouteDestination) String() string {
ip := r.Prefix.Addr()
if r.Zone != "" {
ip = ip.WithZone(r.Zone)
}
return ip.String() + "/" + strconv.Itoa(r.Prefix.Bits())
}
// RouteType describes the type of a route.
type RouteType int
const (
// RouteTypeUnspecified is the unspecified route type.
RouteTypeUnspecified RouteType = iota
// RouteTypeLocal indicates that the destination of this route is an
// address that belongs to this system.
RouteTypeLocal
// RouteTypeUnicast indicates that the destination of this route is a
// "regular" address--one that neither belongs to this host, nor is a
// broadcast/multicast/etc. address.
RouteTypeUnicast
// RouteTypeBroadcast indicates that the destination of this route is a
// broadcast address.
RouteTypeBroadcast
// RouteTypeMulticast indicates that the destination of this route is a
// multicast address.
RouteTypeMulticast
// RouteTypeOther indicates that the route is of some other valid type;
// see the Sys field for the OS-provided route information to determine
// the exact type.
RouteTypeOther
)
func (r RouteType) String() string {
switch r {
case RouteTypeUnspecified:
return "unspecified"
case RouteTypeLocal:
return "local"
case RouteTypeUnicast:
return "unicast"
case RouteTypeBroadcast:
return "broadcast"
case RouteTypeMulticast:
return "multicast"
case RouteTypeOther:
return "other"
default:
return "invalid"
}
}

View File

@@ -0,0 +1,285 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || freebsd
// +build darwin freebsd
package routetable
import (
"bufio"
"fmt"
"net"
"net/netip"
"runtime"
"sort"
"strings"
"syscall"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
"tailscale.com/types/logger"
)
type RouteEntryBSD struct {
// GatewayInterface is the name of the interface specified as a gateway
// for this route, if any.
GatewayInterface string
// GatewayIdx is the index of the interface specified as a gateway for
// this route, if any.
GatewayIdx int
// GatewayAddr is the link-layer address of the gateway for this route,
// if any.
GatewayAddr string
// Flags contains a string representation of common flags for this
// route.
Flags []string
// RawFlags contains the raw flags that were returned by the operating
// system for this route.
RawFlags int
}
// Format implements the fmt.Formatter interface.
func (r RouteEntryBSD) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
var pstart bool
pr := func(format string, args ...any) {
if pstart {
fmt.Fprintf(w, ", "+format, args...)
} else {
fmt.Fprintf(w, format, args...)
pstart = true
}
}
w.WriteString("{")
if r.GatewayInterface != "" {
pr("GatewayInterface: %s", r.GatewayInterface)
}
if r.GatewayIdx > 0 {
pr("GatewayIdx: %d", r.GatewayIdx)
}
if r.GatewayAddr != "" {
pr("GatewayAddr: %s", r.GatewayAddr)
}
pr("Flags: %v", r.Flags)
w.WriteString("}")
}).Format(f, verb)
}
// ipFromRMAddr returns a netip.Addr converted from one of the
// route.Inet{4,6}Addr types.
func ipFromRMAddr(ifs map[int]interfaces.Interface, addr any) netip.Addr {
switch v := addr.(type) {
case *route.Inet4Addr:
return netip.AddrFrom4(v.IP)
case *route.Inet6Addr:
ip := netip.AddrFrom16(v.IP)
if v.ZoneID != 0 {
if iif, ok := ifs[v.ZoneID]; ok {
ip = ip.WithZone(iif.Name)
} else {
ip = ip.WithZone(fmt.Sprint(v.ZoneID))
}
}
return ip
}
return netip.Addr{}
}
// populateGateway populates gateway fields on a RouteEntry/RouteEntryBSD.
func populateGateway(re *RouteEntry, reSys *RouteEntryBSD, ifs map[int]interfaces.Interface, addr any) {
// If the address type has a valid IP, use that.
if ip := ipFromRMAddr(ifs, addr); ip.IsValid() {
re.Gateway = ip
return
}
switch v := addr.(type) {
case *route.LinkAddr:
reSys.GatewayIdx = v.Index
if iif, ok := ifs[v.Index]; ok {
reSys.GatewayInterface = iif.Name
}
var sb strings.Builder
for i, x := range v.Addr {
if i != 0 {
sb.WriteByte(':')
}
fmt.Fprintf(&sb, "%02x", x)
}
reSys.GatewayAddr = sb.String()
}
}
// populateDestination populates the 'Dst' field on a RouteEntry based on the
// RouteMessage's destination and netmask fields.
func populateDestination(re *RouteEntry, ifs map[int]interfaces.Interface, rm *route.RouteMessage) {
dst := rm.Addrs[unix.RTAX_DST]
if dst == nil {
return
}
ip := ipFromRMAddr(ifs, dst)
if !ip.IsValid() {
return
}
if ip.Is4() {
re.Family = 4
} else {
re.Family = 6
}
re.Dst = RouteDestination{
Prefix: netip.PrefixFrom(ip, 32), // default if nothing more specific
}
// If the RTF_HOST flag is set, then this is a host route and there's
// no netmask in this RouteMessage.
if rm.Flags&unix.RTF_HOST != 0 {
return
}
// As above if there's no netmask in the list of addrs
if len(rm.Addrs) < unix.RTAX_NETMASK || rm.Addrs[unix.RTAX_NETMASK] == nil {
return
}
nm := ipFromRMAddr(ifs, rm.Addrs[unix.RTAX_NETMASK])
if !ip.IsValid() {
return
}
// Count the number of bits in the netmask IP and use that to make our prefix.
ones, _ /* bits */ := net.IPMask(nm.AsSlice()).Size()
// Print this ourselves instead of using netip.Prefix so that we don't
// lose the zone (since netip.Prefix strips that).
//
// NOTE(andrew): this doesn't print the same values as the 'netstat' tool
// for some addresses on macOS, and I have no idea why. Specifically,
// 'netstat -rn' will show something like:
// ff00::/8 ::1 UmCI lo0
//
// But we will get:
// destination=ff00::/40 [...]
//
// The netmask that we get back from FetchRIB has 32 more bits in it
// than netstat prints, but only for multicast routes.
//
// For consistency's sake, we're going to do the same here so that we
// get the same values as netstat returns.
if runtime.GOOS == "darwin" && ip.Is6() && ip.IsMulticast() && ones > 32 {
ones -= 32
}
re.Dst = RouteDestination{
Prefix: netip.PrefixFrom(ip, ones),
Zone: ip.Zone(),
}
}
// routeEntryFromMsg returns a RouteEntry from a single route.Message
// returned by the operating system.
func routeEntryFromMsg(ifsByIdx map[int]interfaces.Interface, msg route.Message) (RouteEntry, bool) {
rm, ok := msg.(*route.RouteMessage)
if !ok {
return RouteEntry{}, false
}
// Ignore things that we don't understand
if rm.Version < 3 || rm.Version > 5 {
return RouteEntry{}, false
}
if rm.Type != rmExpectedType {
return RouteEntry{}, false
}
if len(rm.Addrs) < unix.RTAX_GATEWAY {
return RouteEntry{}, false
}
if rm.Flags&skipFlags != 0 {
return RouteEntry{}, false
}
reSys := RouteEntryBSD{
RawFlags: rm.Flags,
}
for fv, fs := range flags {
if rm.Flags&fv == fv {
reSys.Flags = append(reSys.Flags, fs)
}
}
sort.Strings(reSys.Flags)
re := RouteEntry{}
hasFlag := func(f int) bool { return rm.Flags&f != 0 }
switch {
case hasFlag(unix.RTF_LOCAL):
re.Type = RouteTypeLocal
case hasFlag(unix.RTF_BROADCAST):
re.Type = RouteTypeBroadcast
case hasFlag(unix.RTF_MULTICAST):
re.Type = RouteTypeMulticast
// From the manpage: "host entry (net otherwise)"
case !hasFlag(unix.RTF_HOST):
re.Type = RouteTypeUnicast
default:
re.Type = RouteTypeOther
}
populateDestination(&re, ifsByIdx, rm)
if unix.RTAX_GATEWAY < len(rm.Addrs) {
populateGateway(&re, &reSys, ifsByIdx, rm.Addrs[unix.RTAX_GATEWAY])
}
if outif, ok := ifsByIdx[rm.Index]; ok {
re.Interface = outif.Name
}
re.Sys = reSys
return re, true
}
// Get returns route entries from the system route table, limited to at most
// 'max' results.
func Get(max int) ([]RouteEntry, error) {
// Fetching the list of interfaces can race with fetching our route
// table, but we do it anyway since it's helpful for debugging.
ifs, err := interfaces.GetList()
if err != nil {
return nil, err
}
ifsByIdx := make(map[int]interfaces.Interface)
for _, iif := range ifs {
ifsByIdx[iif.Index] = iif
}
rib, err := route.FetchRIB(syscall.AF_UNSPEC, ribType, 0)
if err != nil {
return nil, err
}
msgs, err := route.ParseRIB(parseType, rib)
if err != nil {
return nil, err
}
var ret []RouteEntry
for _, m := range msgs {
re, ok := routeEntryFromMsg(ifsByIdx, m)
if ok {
ret = append(ret, re)
if len(ret) == max {
break
}
}
}
return ret, nil
}

View File

@@ -0,0 +1,435 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || freebsd
// +build darwin freebsd
package routetable
import (
"fmt"
"net"
"net/netip"
"reflect"
"runtime"
"testing"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
)
func TestRouteEntryFromMsg(t *testing.T) {
ifs := map[int]interfaces.Interface{
1: {
Interface: &net.Interface{
Name: "iface0",
},
},
2: {
Interface: &net.Interface{
Name: "tailscale0",
},
},
}
ip4 := func(s string) *route.Inet4Addr {
ip := netip.MustParseAddr(s)
return &route.Inet4Addr{IP: ip.As4()}
}
ip6 := func(s string) *route.Inet6Addr {
ip := netip.MustParseAddr(s)
return &route.Inet6Addr{IP: ip.As16()}
}
ip6zone := func(s string, idx int) *route.Inet6Addr {
ip := netip.MustParseAddr(s)
return &route.Inet6Addr{IP: ip.As16(), ZoneID: idx}
}
link := func(idx int, addr string) *route.LinkAddr {
if _, found := ifs[idx]; !found {
panic("index not found")
}
ret := &route.LinkAddr{
Index: idx,
}
if addr != "" {
ret.Addr = make([]byte, 6)
fmt.Sscanf(addr, "%02x:%02x:%02x:%02x:%02x:%02x",
&ret.Addr[0],
&ret.Addr[1],
&ret.Addr[2],
&ret.Addr[3],
&ret.Addr[4],
&ret.Addr[5],
)
}
return ret
}
type testCase struct {
name string
msg *route.RouteMessage
want RouteEntry
fail bool
}
testCases := []testCase{
{
name: "BasicIPv4",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("1.2.3.4"), // dst
ip4("1.2.3.1"), // gateway
ip4("255.255.255.0"), // netmask
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
Gateway: netip.MustParseAddr("1.2.3.1"),
Sys: RouteEntryBSD{},
},
},
{
name: "BasicIPv6",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip6("fd7a:115c:a1e0::"), // dst
ip6("1234::"), // gateway
ip6("ffff:ffff:ffff::"), // netmask
},
},
want: RouteEntry{
Family: 6,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/48")},
Gateway: netip.MustParseAddr("1234::"),
Sys: RouteEntryBSD{},
},
},
{
name: "IPv6WithZone",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip6zone("fe80::", 2), // dst
ip6("1234::"), // gateway
ip6("ffff:ffff:ffff:ffff::"), // netmask
},
},
want: RouteEntry{
Family: 6,
Type: RouteTypeUnicast, // TODO
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "tailscale0"},
Gateway: netip.MustParseAddr("1234::"),
Sys: RouteEntryBSD{},
},
},
{
name: "IPv6WithUnknownZone",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip6zone("fe80::", 4), // dst
ip6("1234::"), // gateway
ip6("ffff:ffff:ffff:ffff::"), // netmask
},
},
want: RouteEntry{
Family: 6,
Type: RouteTypeUnicast, // TODO
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fe80::/64"), Zone: "4"},
Gateway: netip.MustParseAddr("1234::"),
Sys: RouteEntryBSD{},
},
},
{
name: "DefaultIPv4",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("0.0.0.0"), // dst
ip4("1.2.3.4"), // gateway
ip4("0.0.0.0"), // netmask
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: defaultRouteIPv4,
Gateway: netip.MustParseAddr("1.2.3.4"),
Sys: RouteEntryBSD{},
},
},
{
name: "DefaultIPv6",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip6("0::"), // dst
ip6("1234::"), // gateway
ip6("0::"), // netmask
},
},
want: RouteEntry{
Family: 6,
Type: RouteTypeUnicast,
Dst: defaultRouteIPv6,
Gateway: netip.MustParseAddr("1234::"),
Sys: RouteEntryBSD{},
},
},
{
name: "ShortAddrs",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("1.2.3.4"), // dst
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
Sys: RouteEntryBSD{},
},
},
{
name: "TailscaleIPv4",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("100.64.0.0"), // dst
link(2, ""),
ip4("255.192.0.0"), // netmask
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
Sys: RouteEntryBSD{
GatewayInterface: "tailscale0",
GatewayIdx: 2,
},
},
},
{
name: "Flags",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("1.2.3.4"), // dst
ip4("1.2.3.1"), // gateway
ip4("255.255.255.0"), // netmask
},
Flags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
Gateway: netip.MustParseAddr("1.2.3.1"),
Sys: RouteEntryBSD{
Flags: []string{"gateway", "static", "up"},
RawFlags: unix.RTF_STATIC | unix.RTF_GATEWAY | unix.RTF_UP,
},
},
},
{
name: "SkipNoAddrs",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{},
},
fail: true,
},
{
name: "SkipBadVersion",
msg: &route.RouteMessage{
Version: 1,
},
fail: true,
},
{
name: "SkipBadType",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType + 1,
},
fail: true,
},
{
name: "OutputIface",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Index: 1,
Addrs: []route.Addr{
ip4("1.2.3.4"), // dst
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.4/32")},
Interface: "iface0",
Sys: RouteEntryBSD{},
},
},
{
name: "GatewayMAC",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("100.64.0.0"), // dst
link(1, "01:02:03:04:05:06"),
ip4("255.192.0.0"), // netmask
},
},
want: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("100.64.0.0/10")},
Sys: RouteEntryBSD{
GatewayAddr: "01:02:03:04:05:06",
GatewayInterface: "iface0",
GatewayIdx: 1,
},
},
},
}
if runtime.GOOS == "darwin" {
testCases = append(testCases,
testCase{
name: "SkipFlags",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Addrs: []route.Addr{
ip4("1.2.3.4"), // dst
ip4("1.2.3.1"), // gateway
ip4("255.255.255.0"), // netmask
},
Flags: unix.RTF_UP | skipFlags,
},
fail: true,
},
testCase{
name: "NetmaskAdjust",
msg: &route.RouteMessage{
Version: 3,
Type: rmExpectedType,
Flags: unix.RTF_MULTICAST,
Addrs: []route.Addr{
ip6("ff00::"), // dst
ip6("1234::"), // gateway
ip6("ffff:ffff:ff00::"), // netmask
},
},
want: RouteEntry{
Family: 6,
Type: RouteTypeMulticast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("ff00::/8")},
Gateway: netip.MustParseAddr("1234::"),
Sys: RouteEntryBSD{
Flags: []string{"multicast"},
RawFlags: unix.RTF_MULTICAST,
},
},
},
)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
re, ok := routeEntryFromMsg(ifs, tc.msg)
if wantOk := !tc.fail; ok != wantOk {
t.Fatalf("ok = %v; want %v", ok, wantOk)
}
if !reflect.DeepEqual(re, tc.want) {
t.Fatalf("RouteEntry mismatch:\n got: %+v\nwant: %+v", re, tc.want)
}
})
}
}
func TestRouteEntryFormatting(t *testing.T) {
testCases := []struct {
re RouteEntry
want string
}{
{
re: RouteEntry{
Family: 4,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("1.2.3.0/24")},
Interface: "en0",
Sys: RouteEntryBSD{
GatewayInterface: "en0",
Flags: []string{"static", "up"},
},
},
want: `{Family: IPv4, Dst: 1.2.3.0/24, Interface: en0, Sys: {GatewayInterface: en0, Flags: [static up]}}`,
},
{
re: RouteEntry{
Family: 6,
Type: RouteTypeUnicast,
Dst: RouteDestination{Prefix: netip.MustParsePrefix("fd7a:115c:a1e0::/24")},
Interface: "en0",
Sys: RouteEntryBSD{
GatewayIdx: 3,
Flags: []string{"static", "up"},
},
},
want: `{Family: IPv6, Dst: fd7a:115c:a1e0::/24, Interface: en0, Sys: {GatewayIdx: 3, Flags: [static up]}}`,
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got := fmt.Sprint(tc.re)
if got != tc.want {
t.Fatalf("RouteEntry.String() mismatch\n got: %q\nwant: %q", got, tc.want)
}
})
}
}
func TestGetRouteTable(t *testing.T) {
routes, err := Get(1000)
if err != nil {
t.Fatal(err)
}
// Basic assertion: we have at least one 'default' route
var (
hasDefault bool
)
for _, route := range routes {
if route.Dst == defaultRouteIPv4 || route.Dst == defaultRouteIPv6 {
hasDefault = true
}
}
if !hasDefault {
t.Errorf("expected at least one default route; routes=%v", routes)
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin
// +build darwin
package routetable
import "golang.org/x/sys/unix"
const (
ribType = unix.NET_RT_DUMP2
parseType = unix.NET_RT_IFLIST2
rmExpectedType = unix.RTM_GET2
// Skip routes that were cloned from a parent
skipFlags = unix.RTF_WASCLONED
)
var flags = map[int]string{
unix.RTF_BLACKHOLE: "blackhole",
unix.RTF_BROADCAST: "broadcast",
unix.RTF_GATEWAY: "gateway",
unix.RTF_GLOBAL: "global",
unix.RTF_HOST: "host",
unix.RTF_IFSCOPE: "ifscope",
unix.RTF_MULTICAST: "multicast",
unix.RTF_REJECT: "reject",
unix.RTF_ROUTER: "router",
unix.RTF_STATIC: "static",
unix.RTF_UP: "up",
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build freebsd
// +build freebsd
package routetable
import "golang.org/x/sys/unix"
const (
ribType = unix.NET_RT_DUMP
parseType = unix.NET_RT_IFLIST
rmExpectedType = unix.RTM_GET
// Nothing to skip
skipFlags = 0
)
var flags = map[int]string{
unix.RTF_BLACKHOLE: "blackhole",
unix.RTF_BROADCAST: "broadcast",
unix.RTF_GATEWAY: "gateway",
unix.RTF_HOST: "host",
unix.RTF_MULTICAST: "multicast",
unix.RTF_REJECT: "reject",
unix.RTF_STATIC: "static",
unix.RTF_UP: "up",
}

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