Compare commits

...

68 Commits

Author SHA1 Message Date
Aaron Klotz
a30a7198be VERSION.txt: this is v1.50.0
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-09-25 09:59:53 -06:00
Brad Fitzpatrick
04fabcd359 ipn/{ipnlocal,localapi}, cli: add debug force-netmap-update
For loading testing & profiling the cost of full netmap updates.

Updates #1909

Change-Id: I0afdf5de9967f8d95c7f81d5b531ed1c92c3208f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-24 14:44:18 -07:00
Anton Tolchanov
75dbd71f49 api.md: document the invalid field in Get Key API response
Updates tailscale/terraform-provider-tailscale#144

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2023-09-24 09:47:40 -05:00
Brad Fitzpatrick
241c983920 net/tstun: use untyped consts, simplify DefaultMTU func
Updates #cleanup

Change-Id: Ic9ad1d6134818699f777c66a31024e846dfdc5d4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-23 12:20:19 -07:00
Brad Fitzpatrick
3b32d6c679 wgengine/magicsock, controlclient, net/dns: reduce some logspam
Updates #cleanup

Change-Id: I78b0697a01e94baa33f3de474b591e616fa5e6af
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-23 11:52:47 -07:00
Val
08302c0731 Revert "wgengine/netstack: use buffer pools for UDP packet forwarding"
This reverts commit fb2f3e4741.

Signed-off-by: Val <valerie@tailscale.com>
2023-09-22 10:56:43 -07:00
Val
6cc5b272d8 Revert "wgengine,net,ipn,disco: split up and define different types of MTU"
This reverts commit 059051c58a.

Signed-off-by: Val <valerie@tailscale.com>
2023-09-22 10:56:43 -07:00
Val
059051c58a wgengine,net,ipn,disco: split up and define different types of MTU
Prepare for path MTU discovery by splitting up the concept of
DefaultMTU() into the concepts of the Tailscale TUN MTU, MTUs of
underlying network interfaces, minimum "safe" TUN MTU, user configured
TUN MTU, probed path MTU to a peer, and maximum probed MTU. Add a set
of likely MTUs to probe.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-22 10:15:05 -07:00
Val
fb2f3e4741 wgengine/netstack: use buffer pools for UDP packet forwarding
Use buffer pools for UDP packet forwarding to prepare for increasing the
forwarded UDP packet size for peer path MTU discovery.

Updates #311

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Val <valerie@tailscale.com>
2023-09-22 10:15:05 -07:00
License Updater
81e8335e23 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-09-21 14:50:43 -07:00
Flakes Updater
b83804cc82 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-09-21 14:32:21 -07:00
Joe Tsai
36242904f1 go.mod: update github.com/go-json-experiment/json (#9508)
Update github.com/go-json-experiment/json to the latest version
and fix the build in light of some breaking API changes.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-09-21 14:19:27 -07:00
James Tucker
a82a74f2cf cmd/containerboot: avoid leaking bash scripts after test runs
The test was sending SIGKILL to containerboot, which results in no
signal propagation down to the bash script that is running as a child
process, thus it leaks.

Minor changes to the test daemon script, so that it cleans up the socket
that it creates on exit, and spawns fewer processes.

Fixes tailscale/corp#14833
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-21 13:17:48 -07:00
License Updater
c5006f143f licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-09-21 12:24:46 -07:00
Aaron Klotz
ea6ca78963 release/dist, tool/gocross: add fake "windowsdll" GOOS to gocross
We're going to need to build a DLL containing custom actions for the installer.
This patch adds the foundations of that capability to dist and gocross.

Updates https://github.com/tailscale/corp/issues/13998

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-09-21 13:09:36 -06:00
Joe Tsai
5473d11caa ipn/ipnlocal: perform additional sanity check in diskPath (#9500)
Use filepath.IsLocal to further validate the baseName.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2023-09-21 10:01:27 -07:00
Val
65dc711c76 control,tailcfg,wgengine/magicsock: add nodeAttr to enable/disable peer MTU
Add a nodeAttr to enable/disable peer path MTU discovery.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
Val
95635857dc wgengine/magicsock: replace CanPMTUD() with ShouldPMTUD()
Replace CanPMTUD() with ShouldPMTUD() to check if peer path MTU discovery should
be enabled, in preparation for adding support for enabling/disabling peer MTU
dynamically.

Updated #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
Val
a5ae21a832 wgengine/magicsock: improve don't fragment bit set/get support
Add an enable/disable argument to setDontFragment() in preparation for dynamic
enable/disable of peer path MTU discovery. Add getDontFragment() to get the
status of the don't fragment bit from a socket.

Updates #311

Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
Val
4c793014af wgengine/magicsock: fix don't fragment setsockopt arg for IPv6 on linux
Use IPV6_MTU_DISCOVER for setting don't fragment on IPv6 sockets on Linux (was
using IP_MTU_DISCOVER, the IPv4 arg).

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
Val
055f3fd843 wgengine/magicsock: rename debugPMTUD() to debugEnablePMTUD()
Make the debugknob variable name for enabling peer path MTU discovery match the
env variable name.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
Val
bb3d338334 wgengine/magicsock: rename files for peer MTU
Rename dontfrag* to peermtu* to prepare for more peer MTU related code going
into these files.

Updates #311

Signed-off-by: Val <valerie@tailscale.com>
2023-09-21 04:17:12 -07:00
James Tucker
1c88a77f68 net/dns/publicdns: update Quad9 addresses and references
One Quad9 IPv6 address was incorrect, and an additional group needed
adding. Additionally I checked Cloudflare and included source reference
URLs for both.

Updates #cleanup
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-20 16:55:58 -07:00
Denton Gentry
6e6a510001 go.toolchain.rev: update to Go 1.21.1+
Updates https://github.com/tailscale/tailscale/issues/8419

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-09-20 15:34:32 -07:00
Maisem Ali
4669e7f7d5 cmd/containerboot: add iptables based MSS clamping for ingress/egress proxies
In typical k8s setups, the MTU configured on the eth0 interfaces is typically 1500 which
results in packets being dropped when they make it to proxy pods as the tailscale0 interface
has a 1280 MTU.

As the primary use of this functionality is TCP, add iptables based MSS clamping to allow
connectivity.

Updates #502

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-20 13:58:30 -07:00
Brad Fitzpatrick
546506a54d ipn/ipnlocal: add a test for recent WhoIs regression
This would've prevented #9470.

This used to pass, fails as of 9538e9f970, and passes again
once #9472 is in.

Updates #9470

Change-Id: Iab97666f7a318432fb3b6372a177ab50c55d4697
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-20 13:56:52 -07:00
Joe Tsai
ae89482f25 ipn/ipnlocal: fix LocalBackend.WhoIs for self (#9472)
9538e9f970 broke LocalBackend.WhoIs
where you can no longer lookup yourself in WhoIs.
This occurs because the LocalBackend.peers map only contains peers.
If we fail to lookup a peer, double-check whether it is ourself.

Fixes #9470

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Co-authored-by: Rhea Ghosh <rhea@tailscale.com>
2023-09-20 13:46:19 -07:00
Irbe Krumina
c5b2a365de cmd/k8s-operator: fix egress service name (#9494)
Updates https://github.com/tailscale/tailscale/issues/502

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-09-20 20:58:28 +01:00
Maisem Ali
5f4d76c18c cmd/k8s-operator: rename egress annotation
It was tailscale.com/ts-tailnet-target-ip, which was pretty
redundant. Change it to tailscale.com/tailnet-ip.

Updates #502

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-20 09:47:30 -07:00
Maisem Ali
ea9dd8fabc Revert "ipn/ipnlocal: plumb ExitNodeDNSResolvers for IsWireGuardOnly exit nodes"
This reverts commit f6845b10f6.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-19 20:47:33 -07:00
James Tucker
d52ab181c3 Revert "ipn/ipnlocal: allow Split-DNS and default resolvers with WireGuard nodes"
This reverts commit c7ce4e07e5.

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-19 20:32:04 -07:00
James Tucker
c7ce4e07e5 ipn/ipnlocal: allow Split-DNS and default resolvers with WireGuard nodes
The initial implementation directly mirrored the behavior of Tailscale
exit nodes, where the WireGuard exit node DNS took precedence over other
configuration.

This adjusted implementation treats the WireGuard DNS
resolvers as a lower precedence default resolver than the tailnet
default resolver, and allows split DNS configuration as well.

This also adds test coverage to the existing DNS selection behavior with
respect to default resolvers and split DNS routes for Tailscale exit
nodes above cap 25. There may be some refinement to do in the logic in
those cases, as split DNS may not be working as we intend, though that
would be a pre-existing and separate issue.

Updates #9377
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-19 16:29:57 -07:00
Maisem Ali
3056a98bbd net/tstun: add better logging of natV4Config
It might as well have been spewing out gibberish. This adds
a nicer output format for us to be able to read and identify
whats going on.

Sample output
```
natV4Config{nativeAddr: 100.83.114.95, listenAddrs: [10.32.80.33], dstMasqAddrs: [10.32.80.33: 407 peers]}
```

Fixes tailscale/corp#14650

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-19 16:20:15 -07:00
David Anderson
ed50f360db util/lru: update c.head when deleting the most recently used entry
Fixes tailscale/corp#14747

Signed-off-by: David Anderson <danderson@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-09-19 12:17:50 -07:00
Flakes Updater
4232826cce go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-09-19 10:14:02 -07:00
Will Norris
652f77d236 client/web: switch to using prebuilt web client assets
Updates tailscale/corp#13775

Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Will Norris <will@tailscale.com>
2023-09-19 10:09:54 -07:00
Irbe Krumina
35ad2aafe3 Makefile: make it possibe to pass a custom tag when building dev images (#9461)
Updates #cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-09-19 17:51:22 +01:00
License Updater
1166765559 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-09-19 01:30:21 -07:00
Tom DNetto
c08cf2a9c6 all: declare & plumb IPv6 masquerade address for peer
This PR plumbs through awareness of an IPv6 SNAT/masquerade address from the wire protocol
through to the low-level (tstun / wgengine). This PR is the first in two PRs for implementing
IPv6 NAT support to/from peers.

A subsequent PR will implement the data-plane changes to implement IPv6 NAT - this is just plumbing.

Signed-off-by: Tom DNetto <tom@tailscale.com>
Updates ENG-991
2023-09-18 21:27:36 -07:00
Andrew Dunham
d9ae7d670e net/portmapper: add clientmetric for UPnP error codes
This should allow us to gather a bit more information about errors that
we encounter when creating UPnP mappings. Since we don't have a
"LabelMap" construction for clientmetrics, do what sockstats does and
lazily register a new metric when we see a new code.

Updates #9343

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ibb5aadd6138beb58721f98123debcc7273b611ba
2023-09-18 18:47:24 -04:00
Maisem Ali
19a9d9037f tailcfg: add NodeCapMap
Like PeerCapMap, add a field to `tailcfg.Node` which provides
a map of Capability to raw JSON messages which are deferred to be
parsed later by the application code which cares about the specific
capabilities. This effectively allows us to prototype new behavior
without having to commit to a schema in tailcfg, and it also opens up
the possibilities to develop custom behavior in tsnet applications w/o
having to plumb through application specific data in the MapResponse.

Updates #4217

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Maisem Ali
4da0689c2c tailcfg: add Node.HasCap helpers
This makes a follow up change less noisy.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Maisem Ali
d06b48dd0a tailcfg: add RawMessage
This adds a new RawMessage type backed by string instead of the
json.RawMessage which is backed by []byte. The byte slice makes
the generated views be a lot more defensive than the need to be
which we can get around by using a string instead.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-18 12:00:34 -07:00
Sonia Appasamy
258f16f84b ipn/ipnlocal: add tailnet MagicDNS name to ipn.LoginProfile
Start backfilling MagicDNS suffixes on LoginProfiles.

Updates #9286

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-09-18 13:58:32 -04:00
Brad Fitzpatrick
0d991249e1 types/netmap: remove NetworkMap.{Addresses,MachineStatus}
And convert all callers over to the methods that check SelfNode.

Now we don't have multiple ways to express things in tests (setting
fields on SelfNode vs NetworkMap, sometimes inconsistently) and don't
have multiple ways to check those two fields (often only checking one
or the other).

Updates #9443

Change-Id: I2d7ba1cf6556142d219fae2be6f484f528756e3c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-18 17:08:11 +01:00
Marwan Sulaiman
d25217c9db cmd/tailscale/cli: error when serving foreground if bg already exists
This PR fixes a bug to make sure that we don't allow two configs
exist with duplicate ports

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-18 11:16:01 -04:00
Brad Fitzpatrick
98b5da47e8 types/views: add SliceContainsFunc like slices.ContainsFunc
Needed for a future change.

Updates #cleanup

Change-Id: I6d89ee8a048b3bb1eb9cfb2e5a53c93aed30b021
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-18 16:09:59 +01:00
Maisem Ali
a61caea911 tailcfg: define a type for NodeCapability
Instead of untyped string, add a type to identify these.

Updates #cleanup

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-17 13:16:29 -07:00
Brad Fitzpatrick
3d37328af6 wgengine, proxymap: split out port mapping from Engine to new type
(Continuing quest to remove rando stuff from the "Engine")

Updates #cleanup

Change-Id: I77f39902c2194410c10c054b545d70c9744250b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 20:06:43 +01:00
Brad Fitzpatrick
db2f37d7c6 ipn/ipnlocal: add some test accessors
Updates tailscale/corp#12990

Change-Id: I82801ac4c003d2c7e1352c514adb908dbf01be87
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:35:17 +01:00
Brad Fitzpatrick
9538e9f970 ipn/ipnlocal: keep internal map updated of latest Nodes post mutations
We have some flaky integration tests elsewhere that have no one place
to ask about the state of the world. This makes LocalBackend be that
place (as it's basically there anyway) but doesn't yet add the ForTest
accessor method.

This adds a LocalBackend.peers map[NodeID]NodeView that is
incrementally updated as mutations arrive. And then we start moving
away from using NetMap.Peers at runtime (UpdateStatus no longer uses
it now). And remove another copy of NodeView in the LocalBackend
nodeByAddr map. Change that to point into b.peers instead.

Future changes will then start streaming whole-node-granularity peer
change updates to WatchIPNBus clients, tracking statefully per client
what each has seen. This will get the GUI clients from receiving less
of a JSON storm of updates all the time.

Updates #1909

Change-Id: I14a976ca9f493bdf02ba7e6e05217363dcf422e5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:35:17 +01:00
Brad Fitzpatrick
926c990a09 types/netmap: start phasing out Addresses, add GetAddresses method
NetworkMap.Addresses is redundant with the SelfNode.Addresses. This
works towards a TODO to delete NetworkMap.Addresses and replace it
with a method.

This is similar to #9389.

Updates #cleanup

Change-Id: Id000509ca5d16bb636401763d41bdb5f38513ba0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:16:43 +01:00
Brad Fitzpatrick
fb5ceb03e3 types/netmap: deprecate NetworkMap.MachineStatus, add accessor method
Step 1 of deleting it, per TODO.

Updates #cleanup

Change-Id: I1d3d0165ae5d8b20610227d60640997b73568733
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 19:09:11 +01:00
Brad Fitzpatrick
0f3c279b86 ipn/ipnlocal: delete some unused code
Updates #cleanup

Change-Id: I90b46c476f135124d97288e776c2b428b351b8b8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 16:32:57 +01:00
Brad Fitzpatrick
760b945bc0 ipn/{ipnlocal,ipnstate}: start simplifying UpdateStatus/StatusBuilder
* Remove unnecessary mutexes (there's no concurrency)
* Simplify LocalBackend.UpdateStatus using the StatusBuilder.WantPeers
  field that was added in 0f604923d3, removing passing around some
  method values into func args. And then merge two methods.

More remains, but this is a start.

Updates #9433

Change-Id: Iaf2d7ec6e4e590799f00bae185465a4fd089b822
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-17 15:38:54 +01:00
James Tucker
8ab46952d4 net/ping: fix ICMP echo code field to 0
The code was trying to pass the ICMP protocol number here (1), which is
not a valid code. Many servers will not respond to echo messages with
codes other than 0.

https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml#icmp-parameters-codes-8

Updates #9299
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 17:08:39 -07:00
James Tucker
f6845b10f6 ipn/ipnlocal: plumb ExitNodeDNSResolvers for IsWireGuardOnly exit nodes
This enables installing default resolvers specified by
tailcfg.Node.ExitNodeDNSResolvers when the exit node is selected.

Updates #9377

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 13:58:38 -07:00
James Tucker
e7727db553 tailcfg: add DNS address list for IsWireGuardOnly nodes
Tailscale exit nodes provide DNS service over the peer API, however
IsWireGuardOnly nodes do not have a peer API, and instead need client
DNS parameters passed in their node description.

For Mullvad nodes this will contain the in network 10.64.0.1 address.

Updates #9377

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-15 13:15:18 -07:00
Maisem Ali
335a5aaf9a cmd/k8s-operator: add APISERVER_PROXY env
The kube-apiserver proxy in the operator would only run in
auth proxy mode but thats not always desirable. There are
situations where the proxy should just be a transparent
proxy and not inject auth headers, so do that using a new
env var APISERVER_PROXY and deprecate the AUTH_PROXY env.

THe new env var has three options `false`, `true` and `noauth`.

Updates #8317

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-09-15 09:18:18 -05:00
James Tucker
4c693d2ee8 net/dns/publicdns: update Mullvad DoH server list
The following IPs are not used anymore: 193.19.108.2 and 193.19.108.3.
All of the servers are now named consistently under dns.mullvad.net.
Several new servers were added.

https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/

Updates #5416
Updates #9345

Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 17:48:01 -07:00
Charlotte Brandhorst-Satzkorn
8428a64b56 words: holy mole we need some more mammals
Particularly of the polydactyl kind.

Updates #14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-09-14 14:25:33 -07:00
James Tucker
1858ad65c8 cmd/cloner: do not allocate slices when the source is nil
tailcfg.Node zero-value clone equality checks failed when I added a
[]*foo to the structure, as the zero value and it's clone contained a
different slice header.

Updates #9377
Updates #9408
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 11:36:34 -07:00
James Tucker
85155ddaf3 tailcfg: remove completed TODO from IsWireGuardOnly
Updates #7826
Signed-off-by: James Tucker <james@tailscale.com>
2023-09-14 10:16:18 -07:00
Tyler Smalley
dfefaa5e35 Use parent serve config
Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-09-14 10:22:38 -05:00
Marwan Sulaiman
f3a5bfb1b9 cmd/tailscale/cli: add set serve validations
This PR adds validations for the new new funnel/serve
commands under the following rules:
1. There is always a single config for one port (bg or fg).
2. Foreground configs under the same port cannot co-exists (for now).
3. Background configs can change as long as the serve type is the same.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-14 10:22:38 -05:00
Andrew Lytvynov
7ce1c6f981 .github/workflows: fix slack-action format in govulncheck.yml (#9390)
Currently slack messages for errors fail:
https://github.com/tailscale/tailscale/actions/runs/6159104272/job/16713248204

```
Error: Unexpected token
 in JSON at position 151
```

This is likely due to the line break in the text. Restructure the
message to use separate title/text and fix the slack webhook body.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-09-13 14:36:40 -07:00
Marwan Sulaiman
3421784e37 cmd/tailscale/cli: use optimistic concurrency control on SetServeConfig
This PR uses the etag/if-match pattern to ensure multiple calls
to SetServeConfig are synchronized. It currently errors out and
asks the user to retry but we can add occ retries as a follow up.

Updates #8489

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2023-09-13 15:08:41 -05:00
Brad Fitzpatrick
6e66e5beeb cmd/tsconnect/wasm: pass a netmon to ipnserver.New
It became required as of 6e967446e4

Updates #8052

Change-Id: I08d100534254865293c1beca5beff8e529e4e9ac
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-09-13 12:52:55 -07:00
126 changed files with 2595 additions and 1016 deletions

View File

@@ -27,8 +27,9 @@ jobs:
payload: >
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks>
(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|commit>) of ${{ github.repository }}@${{ github.ref_name }} by ${{ github.event.head_commit.committer.name }}",
"title": "${{ job.status }}: ${{ github.workflow }}",
"title_link": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks",
"text": "${{ github.repository }}@${{ github.sha }}",
"color": "danger"
}]
}

View File

@@ -1,6 +1,7 @@
IMAGE_REPO ?= tailscale/tailscale
SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
TAGS ?= "latest"
vet: ## Run go vet
./tool/go vet ./...
@@ -67,7 +68,7 @@ publishdevimage: ## Build and publish tailscale image to location specified by $
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@@ -75,7 +76,7 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"

View File

@@ -41,16 +41,6 @@ We always require the latest Go release, currently Go 1.21. (While we build
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
required.)
To include the embedded web client (accessed via the `tailscale web` command),
first build the client assets using:
```
./tool/yarn --cwd client/web install
./tool/yarn --cwd client/web build
```
Build the `tailscale` and `tailscaled` binaries:
```
go install tailscale.com/cmd/tailscale{,d}
```

View File

@@ -1 +1 @@
1.49.0
1.50.0

12
api.md
View File

@@ -1434,6 +1434,18 @@ The response is a JSON object with information about the key supplied.
}
```
Response for a revoked (deleted) or expired key will have an `invalid` field set to `true`:
``` jsonc
{
"id": "abc123456CNTRL",
"created": "2022-05-05T18:55:44Z",
"expires": "2022-08-03T18:55:44Z",
"revoked": "2023-04-01T20:50:00Z",
"invalid": true
}
```
<a href="tailnet-keys-key-delete"></a>
## Delete key

View File

@@ -5,9 +5,6 @@
# information into the binaries, so that we can track down user
# issues.
#
# To include the embedded web client, build the web client assets
# before running this script. See README.md for details.
#
# If you're packaging Tailscale for a distro, please consider using
# this script, or executing equivalent commands in your
# distro-specific build system.

View File

@@ -140,6 +140,10 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
all, _ := io.ReadAll(res.Body)
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
}
if res.StatusCode == http.StatusPreconditionFailed {
all, _ := io.ReadAll(res.Body)
return nil, &PreconditionsFailedError{errors.New(errorMessageFromBody(all))}
}
return res, nil
}
if ue, ok := err.(*url.Error); ok {
@@ -170,6 +174,24 @@ func IsAccessDeniedError(err error) bool {
return errors.As(err, &ae)
}
// PreconditionsFailedError is returned when the server responds
// with an HTTP 412 status code.
type PreconditionsFailedError struct {
err error
}
func (e *PreconditionsFailedError) Error() string {
return fmt.Sprintf("Preconditions failed: %v", e.err)
}
func (e *PreconditionsFailedError) Unwrap() error { return e.err }
// IsPreconditionsFailedError reports whether err is or wraps an PreconditionsFailedError.
func IsPreconditionsFailedError(err error) bool {
var ae *PreconditionsFailedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
@@ -198,27 +220,42 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, nil)
return slurp, err
}
func (lc *LocalClient) sendWithHeaders(
ctx context.Context,
method,
path string,
wantStatus int,
body io.Reader,
h http.Header,
) ([]byte, http.Header, error) {
if jr, ok := body.(jsonReader); ok && jr.err != nil {
return nil, jr.err // fail early if there was a JSON marshaling error
return nil, nil, jr.err // fail early if there was a JSON marshaling error
}
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
if err != nil {
return nil, err
return nil, nil, err
}
if h != nil {
req.Header = h
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
return nil, nil, err
}
defer res.Body.Close()
slurp, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
return nil, nil, err
}
if res.StatusCode != wantStatus {
err = fmt.Errorf("%v: %s", res.Status, bytes.TrimSpace(slurp))
return nil, bestError(err, slurp)
return nil, nil, bestError(err, slurp)
}
return slurp, nil
return slurp, res.Header, nil
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
@@ -1093,7 +1130,11 @@ func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config))
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
}
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
@@ -1112,11 +1153,19 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
//
// If the serve config is empty, it returns (nil, nil).
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/serve-config", 200, nil)
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
return getServeConfigFromJSON(body)
sc, err := getServeConfigFromJSON(body)
if err != nil {
return nil, err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
sc.ETag = h.Get("Etag")
return sc, nil
}
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {

View File

@@ -4,8 +4,6 @@
package web
import (
"embed"
"io/fs"
"log"
"net/http"
"net/http/httputil"
@@ -15,36 +13,16 @@ import (
"path/filepath"
"strings"
"tailscale.com/util/must"
prebuilt "github.com/tailscale/web-client-prebuilt"
)
// This contains all files needed to build the frontend assets.
// Because we assign this to the blank identifier, it does not actually embed the files.
// However, this does cause `go mod vendor` to include the files when vendoring the package.
// External packages that use the web client can `go mod vendor`, run `yarn build` to
// build the assets, then those asset bundles will be embedded.
//
//go:embed yarn.lock index.html *.js *.json src/*
var _ embed.FS
//go:embed build/*
var embeddedFS embed.FS
// staticfiles serves static files from the build directory.
var staticfiles http.Handler
func init() {
buildFiles := must.Get(fs.Sub(embeddedFS, "build"))
staticfiles = http.FileServer(http.FS(buildFiles))
}
func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
if devMode {
// When in dev mode, proxy asset requests to the Vite dev server.
cleanup := startDevServer()
return devServerProxy(), cleanup
}
return staticfiles, nil
return http.FileServer(http.FS(prebuilt.FS())), nil
}
// startDevServer starts the JS dev server that does on-demand rebuilding

View File

@@ -32,7 +32,7 @@ export default defineConfig({
],
build: {
outDir: "build",
sourcemap: true,
sourcemap: false,
},
esbuild: {
logOverride: {

View File

@@ -122,6 +122,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
case *types.Slice:
if codegen.ContainsPointers(ft.Elem()) {
n := it.QualifiedName(ft.Elem())
writef("if src.%s != nil {", fname)
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
@@ -137,6 +138,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}
writef("}")
writef("}")
} else {
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
}

View File

@@ -696,6 +696,13 @@ func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []net
if err := cmdSNAT.Run(); err != nil {
return fmt.Errorf("setting up SNAT via iptables failed: %w", err)
}
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
cmdClamp.Stdout = os.Stdout
cmdClamp.Stderr = os.Stderr
if err := cmdClamp.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
}
return nil
}
@@ -731,6 +738,12 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
if err := cmd.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
}
cmdClamp := exec.CommandContext(ctx, argv0, "-t", "mangle", "-A", "FORWARD", "-o", "tailscale0", "-p", "tcp", "-m", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu")
cmdClamp.Stdout = os.Stdout
cmdClamp.Stderr = os.Stderr
if err := cmdClamp.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
}
return nil
}

View File

@@ -330,6 +330,7 @@ func TestContainerBoot(t *testing.T) {
WantCmds: []string{
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
"/usr/bin/iptables -t nat -I PREROUTING 1 -d 100.64.0.1 -j DNAT --to-destination 1.2.3.4",
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
},
},
},
@@ -354,6 +355,7 @@ func TestContainerBoot(t *testing.T) {
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock set --accept-dns=false",
"/usr/bin/iptables -t nat -I PREROUTING 1 ! -i tailscale0 -j DNAT --to-destination 100.99.99.99",
"/usr/bin/iptables -t nat -I POSTROUTING 1 --destination 100.99.99.99 -j SNAT --to-source 100.64.0.1",
"/usr/bin/iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu",
},
},
},
@@ -687,7 +689,7 @@ func TestContainerBoot(t *testing.T) {
t.Fatalf("starting containerboot: %v", err)
}
defer func() {
cmd.Process.Signal(unix.SIGKILL)
cmd.Process.Signal(unix.SIGTERM)
cmd.Process.Wait()
}()

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# This is a fake tailscale CLI that records its arguments, symlinks a
# This is a fake tailscale daemon that records its arguments, symlinks a
# fake LocalAPI socket into place, and does nothing until terminated.
#
# It is used by main_test.go to test the behavior of containerboot.
@@ -33,5 +33,6 @@ if [[ -z "$socket" ]]; then
fi
ln -s "$TS_TEST_SOCKET" "$socket"
trap 'rm -f "$socket"' EXIT
while true; do sleep 1; done
while sleep 10; do :; done

View File

@@ -169,6 +169,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/maps from tailscale.com/tailcfg
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http

View File

@@ -47,12 +47,11 @@ func main() {
tailscale.I_Acknowledge_This_API_Is_Unstable = true
var (
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
)
var opts []kzap.Opts
@@ -70,10 +69,8 @@ func main() {
s, tsClient := initTSNet(zlog)
defer s.Close()
restConfig := config.GetConfigOrDie()
if shouldRunAuthProxy {
launchAuthProxy(zlog, restConfig, s)
}
startReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags)
maybeLaunchAPIServerProxy(zlog, restConfig, s)
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags)
}
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
@@ -180,9 +177,9 @@ waitOnline:
return s, tsClient
}
// startReconcilers starts the controller-runtime manager and registers the
// ServiceReconciler.
func startReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) {
// runReconcilers starts the controller-runtime manager and registers the
// ServiceReconciler. It blocks forever.
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags string) {
var (
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
)

View File

@@ -218,7 +218,7 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
},
},
Spec: corev1.ServiceSpec{
ExternalName: fmt.Sprintf("%s.operator-ns.svc", shortName),
ExternalName: fmt.Sprintf("%s.operator-ns.svc.cluster.local", shortName),
Type: corev1.ServiceTypeExternalName,
Selector: nil,
},

View File

@@ -45,12 +45,52 @@ func addWhoIsToRequest(r *http.Request, who *apitype.WhoIsResponse) *http.Reques
var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
// launchAuthProxy launches the auth proxy, which is a small HTTP server that
// authenticates requests using the Tailscale LocalAPI and then proxies them to
// the kube-apiserver.
func launchAuthProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server) {
type apiServerProxyMode int
const (
apiserverProxyModeDisabled apiServerProxyMode = iota
apiserverProxyModeEnabled
apiserverProxyModeNoAuth
)
func parseAPIProxyMode() apiServerProxyMode {
haveAuthProxyEnv := os.Getenv("AUTH_PROXY") != ""
haveAPIProxyEnv := os.Getenv("APISERVER_PROXY") != ""
switch {
case haveAPIProxyEnv && haveAuthProxyEnv:
log.Fatal("AUTH_PROXY and APISERVER_PROXY are mutually exclusive")
case haveAuthProxyEnv:
var authProxyEnv = defaultBool("AUTH_PROXY", false) // deprecated
if authProxyEnv {
return apiserverProxyModeEnabled
}
return apiserverProxyModeDisabled
case haveAPIProxyEnv:
var apiProxyEnv = defaultEnv("APISERVER_PROXY", "") // true, false or "noauth"
switch apiProxyEnv {
case "true":
return apiserverProxyModeEnabled
case "false", "":
return apiserverProxyModeDisabled
case "noauth":
return apiserverProxyModeNoAuth
default:
panic(fmt.Sprintf("unknown APISERVER_PROXY value %q", apiProxyEnv))
}
}
return apiserverProxyModeDisabled
}
// maybeLaunchAPIServerProxy launches the auth proxy, which is a small HTTP server
// that authenticates requests using the Tailscale LocalAPI and then proxies
// them to the kube-apiserver.
func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.Server) {
mode := parseAPIProxyMode()
if mode == apiserverProxyModeDisabled {
return
}
hostinfo.SetApp("k8s-operator-proxy")
startlog := zlog.Named("launchAuthProxy")
startlog := zlog.Named("launchAPIProxy")
cfg, err := restConfig.TransportConfig()
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
@@ -69,18 +109,18 @@ func launchAuthProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, s *tsnet.
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy").Infof, mode)
}
// authProxy is an http.Handler that authenticates requests using the Tailscale
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
// LocalAPI and then proxies them to the Kubernetes API.
type authProxy struct {
type apiserverProxy struct {
logf logger.Logf
lc *tailscale.LocalClient
rp *httputil.ReverseProxy
}
func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
h.logf("failed to authenticate caller: %v", err)
@@ -91,28 +131,38 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.rp.ServeHTTP(w, addWhoIsToRequest(r, who))
}
// runAuthProxy runs an HTTP server that authenticates requests using the
// runAPIServerProxy runs an HTTP server that authenticates requests using the
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
// It listens on :443 and uses the Tailscale HTTPS certificate.
// s will be started if it is not already running.
// rt is used to proxy requests to the Kubernetes API.
//
// mode controls how the proxy behaves:
// - apiserverProxyModeDisabled: the proxy is not started.
// - apiserverProxyModeEnabled: the proxy is started and requests are impersonated using the
// caller's identity from the Tailscale LocalAPI.
// - apiserverProxyModeNoAuth: the proxy is started and requests are not impersonated and
// are passed through to the Kubernetes API.
//
// It never returns.
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf, mode apiServerProxyMode) {
if mode == apiserverProxyModeDisabled {
return
}
ln, err := s.Listen("tcp", ":443")
if err != nil {
log.Fatalf("could not listen on :443: %v", err)
}
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
if err != nil {
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
}
lc, err := s.LocalClient()
if err != nil {
log.Fatalf("could not get local client: %v", err)
}
ap := &authProxy{
ap := &apiserverProxy{
logf: logf,
lc: lc,
rp: &httputil.ReverseProxy{
@@ -120,6 +170,12 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
// Replace the URL with the Kubernetes APIServer.
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
if mode == apiserverProxyModeNoAuth {
// If we are not providing authentication, then we are just
// proxying to the Kubernetes API, so we don't need to do
// anything else.
return
}
// We want to proxy to the Kubernetes API, but we want to use
// the caller's identity to do so. We do this by impersonating
@@ -157,7 +213,7 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
Handler: ap,
}
if err := hs.ServeTLS(ln, "", ""); err != nil {
log.Fatalf("runAuthProxy: failed to serve %v", err)
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
}
}
@@ -177,7 +233,7 @@ type impersonateRule struct {
// addImpersonationHeaders adds the appropriate headers to r to impersonate the
// caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
// in the context by the authProxy.
// in the context by the apiserverProxy.
func addImpersonationHeaders(r *http.Request) error {
who := whoIsFromRequest(r)
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)

View File

@@ -45,15 +45,15 @@ func TestImpersonationHeaders(t *testing.T) {
emailish: "foo@example.com",
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`{"impersonate":{"groups":["group1","group2"]}}`),
[]byte(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
[]byte(`{"impersonate":{"groups":["group4"]}}`),
[]byte(`{"impersonate":{"groups":["group2"]}}`), // duplicate
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group2"]}}`), // duplicate
// These should be ignored, but should parse correctly.
[]byte(`{}`),
[]byte(`{"impersonate":{}}`),
[]byte(`{"impersonate":{"groups":[]}}`),
tailcfg.RawMessage(`{}`),
tailcfg.RawMessage(`{"impersonate":{}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":[]}}`),
},
},
wantHeaders: http.Header{
@@ -67,7 +67,7 @@ func TestImpersonationHeaders(t *testing.T) {
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`{"impersonate":{"groups":["group1"]}}`),
tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`),
},
},
wantHeaders: http.Header{
@@ -81,7 +81,7 @@ func TestImpersonationHeaders(t *testing.T) {
tags: []string{"tag:foo", "tag:bar"},
capMap: tailcfg.PeerCapMap{
capabilityName: {
[]byte(`[]`),
tailcfg.RawMessage(`[]`),
},
},
wantHeaders: http.Header{},

View File

@@ -39,10 +39,11 @@ const (
FinalizerName = "tailscale.com/finalizer"
// Annotations settable by users on services.
AnnotationExpose = "tailscale.com/expose"
AnnotationTags = "tailscale.com/tags"
AnnotationHostname = "tailscale.com/hostname"
AnnotationTailnetTargetIP = "tailscale.com/ts-tailnet-target-ip"
AnnotationExpose = "tailscale.com/expose"
AnnotationTags = "tailscale.com/tags"
AnnotationHostname = "tailscale.com/hostname"
annotationTailnetTargetIPOld = "tailscale.com/ts-tailnet-target-ip"
AnnotationTailnetTargetIP = "tailscale.com/tailnet-ip"
// Annotations settable by users on ingresses.
AnnotationFunnel = "tailscale.com/funnel"

View File

@@ -77,7 +77,8 @@ func (a *ServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request
} else if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get svc: %w", err)
}
if !svc.DeletionTimestamp.IsZero() || !a.shouldExpose(svc) && !a.hasTailnetTargetAnnotation(svc) {
targetIP := a.tailnetTargetAnnotation(svc)
if !svc.DeletionTimestamp.IsZero() || !a.shouldExpose(svc) && targetIP == "" {
logger.Debugf("service is being deleted or is (no longer) referring to Tailscale ingress/egress, ensuring any created resources are cleaned up")
return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc)
}
@@ -170,8 +171,8 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
sts.ClusterTargetIP = svc.Spec.ClusterIP
a.managedIngressProxies.Add(svc.UID)
gaugeIngressProxies.Set(int64(a.managedIngressProxies.Len()))
} else if a.hasTailnetTargetAnnotation(svc) {
sts.TailnetTargetIP = svc.Annotations[AnnotationTailnetTargetIP]
} else if ip := a.tailnetTargetAnnotation(svc); ip != "" {
sts.TailnetTargetIP = ip
a.managedEgressProxies.Add(svc.UID)
gaugeEgressProxies.Set(int64(a.managedEgressProxies.Len()))
}
@@ -182,8 +183,11 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
return fmt.Errorf("failed to provision: %w", err)
}
if a.hasTailnetTargetAnnotation(svc) {
headlessSvcName := hsvc.Name + "." + hsvc.Namespace + ".svc"
if sts.TailnetTargetIP != "" {
// TODO (irbekrm): cluster.local is the default DNS name, but
// can be changed by users. Make this configurable or figure out
// how to discover the DNS name from within operator
headlessSvcName := hsvc.Name + "." + hsvc.Namespace + ".svc.cluster.local"
if svc.Spec.ExternalName != headlessSvcName || svc.Spec.Type != corev1.ServiceTypeExternalName {
svc.Spec.ExternalName = headlessSvcName
svc.Spec.Selector = nil
@@ -261,8 +265,16 @@ func (a *ServiceReconciler) hasExposeAnnotation(svc *corev1.Service) bool {
return svc != nil && svc.Annotations[AnnotationExpose] == "true"
}
// hasTailnetTargetAnnotation reports whether Service has a
// tailscale.com/ts-tailnet-target-ip annotation set
func (a *ServiceReconciler) hasTailnetTargetAnnotation(svc *corev1.Service) bool {
return svc != nil && svc.Annotations[AnnotationTailnetTargetIP] != ""
// hasTailnetTargetAnnotation returns the value of tailscale.com/tailnet-ip
// annotation or of the deprecated tailscale.com/ts-tailnet-target-ip
// annotation. If neither is set, it returns an empty string. If both are set,
// it returns the value of the new annotation.
func (a *ServiceReconciler) tailnetTargetAnnotation(svc *corev1.Service) string {
if svc == nil {
return ""
}
if ip := svc.Annotations[AnnotationTailnetTargetIP]; ip != "" {
return ip
}
return svc.Annotations[annotationTailnetTargetIPOld]
}

View File

@@ -42,6 +42,7 @@ import (
"github.com/dsnet/try"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/cmpx"
@@ -75,13 +76,13 @@ func main() {
func processStream(r io.Reader) (err error) {
defer try.Handle(&err)
dec := jsonv2.NewDecoder(os.Stdin)
dec := jsontext.NewDecoder(os.Stdin)
for {
processValue(dec)
}
}
func processValue(dec *jsonv2.Decoder) {
func processValue(dec *jsontext.Decoder) {
switch dec.PeekKind() {
case '[':
processArray(dec)
@@ -92,7 +93,7 @@ func processValue(dec *jsonv2.Decoder) {
}
}
func processArray(dec *jsonv2.Decoder) {
func processArray(dec *jsontext.Decoder) {
try.E1(dec.ReadToken()) // parse '['
for dec.PeekKind() != ']' {
processValue(dec)
@@ -100,7 +101,7 @@ func processArray(dec *jsonv2.Decoder) {
try.E1(dec.ReadToken()) // parse ']'
}
func processObject(dec *jsonv2.Decoder) {
func processObject(dec *jsontext.Decoder) {
var hasTraffic bool
var rawMsg []byte
try.E1(dec.ReadToken()) // parse '{'

View File

@@ -138,6 +138,11 @@ var debugCmd = &ffcli.Command{
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
},
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "force a full no-op netmap update (for load testing)",
},
{
Name: "control-knobs",
Exec: debugControlKnobs,

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"net"
"os"
"slices"
"strconv"
"strings"
@@ -147,15 +146,13 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
//
// verifyFunnelEnabled may refresh the local state and modify the st input.
func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status, port uint16) error {
hasFunnelAttrs := func(attrs []string) bool {
hasHTTPS := slices.Contains(attrs, tailcfg.CapabilityHTTPS)
hasFunnel := slices.Contains(attrs, tailcfg.NodeAttrFunnel)
return hasHTTPS && hasFunnel
hasFunnelAttrs := func(selfNode *ipnstate.PeerStatus) bool {
return selfNode.HasCap(tailcfg.CapabilityHTTPS) && selfNode.HasCap(tailcfg.NodeAttrFunnel)
}
if hasFunnelAttrs(st.Self.Capabilities) {
if hasFunnelAttrs(st.Self) {
return nil // already enabled
}
enableErr := e.enableFeatureInteractive(ctx, "funnel", hasFunnelAttrs)
enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel)
st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block
switch {
case statusErr != nil:

View File

@@ -18,7 +18,6 @@ import (
"path/filepath"
"reflect"
"runtime"
"slices"
"sort"
"strconv"
"strings"
@@ -269,9 +268,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
// on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs.
e.enableFeatureInteractive(ctx, "serve", func(caps []string) bool {
return slices.Contains(caps, tailcfg.CapabilityHTTPS)
})
e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS)
}
srcPort, err := parseServePort(srcPortStr)
@@ -829,7 +826,7 @@ func parseServePort(s string) (uint16, error) {
//
// 2023-08-09: The only valid feature values are "serve" and "funnel".
// This can be moved to some CLI lib when expanded past serve/funnel.
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, hasRequiredCapabilities func(caps []string) bool) (err error) {
func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) {
info, err := e.lc.QueryFeature(ctx, feature)
if err != nil {
return err
@@ -875,7 +872,16 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
return err
}
if nm := n.NetMap; nm != nil && nm.SelfNode.Valid() {
if hasRequiredCapabilities(nm.SelfNode.Capabilities().AsSlice()) {
gotAll := true
for _, c := range caps {
if !nm.SelfNode.HasCap(c) {
// The feature is not yet enabled.
// Continue blocking until it is.
gotAll = false
break
}
}
if gotAll {
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_enabled", feature), 1)
fmt.Fprintln(os.Stdout, "Success.")
return nil

View File

@@ -15,7 +15,6 @@ import (
"os/signal"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
@@ -233,9 +232,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// on, enableFeatureInteractive will error. For now, we hide that
// error and maintain the previous behavior (prior to 2023-08-15)
// of letting them edit the serve config before enabling certs.
if err := e.enableFeatureInteractive(ctx, "serve", func(caps []string) bool {
return slices.Contains(caps, tailcfg.CapabilityHTTPS)
}); err != nil {
if err := e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS); err != nil {
return fmt.Errorf("error enabling https feature: %w", err)
}
}
@@ -266,6 +263,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
if turnOff {
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount)
} else {
if err := e.validateConfig(parentSC, srvPort, srvType); err != nil {
return err
}
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
msg = e.messageForPort(sc, st, dnsName, srvPort)
}
@@ -275,6 +275,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
}
if err := e.lc.SetServeConfig(ctx, parentSC); err != nil {
if tailscale.IsPreconditionsFailedError(err) {
fmt.Fprintln(os.Stderr, "Another client is changing the serve config; please try again.")
}
return err
}
@@ -298,6 +301,57 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
}
}
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
sc, isFg := findConfig(sc, port)
if sc == nil {
return nil
}
if isFg {
return errors.New("foreground already exists under this port")
}
if !e.bg {
return errors.New("background serve already exists under this port")
}
existingServe := serveFromPortHandler(sc.TCP[port])
if wantServe != existingServe {
return fmt.Errorf("want %q but port is already serving %q", wantServe, existingServe)
}
return nil
}
func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
switch {
case tcp.HTTP:
return serveTypeHTTP
case tcp.HTTPS:
return serveTypeHTTPS
case tcp.TerminateTLS != "":
return serveTypeTLSTerminatedTCP
case tcp.TCPForward != "":
return serveTypeTCP
default:
return -1
}
}
// findConfig finds a config that contains the given port, which can be
// the top level background config or an inner foreground one. The second
// result is true if it's foreground
func findConfig(sc *ipn.ServeConfig, port uint16) (*ipn.ServeConfig, bool) {
if sc == nil {
return nil, false
}
if _, ok := sc.TCP[port]; ok {
return sc, false
}
for _, sc := range sc.Foreground {
if _, ok := sc.TCP[port]; ok {
return sc, true
}
}
return nil, false
}
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
// update serve config based on the type
switch srvType {
@@ -742,13 +796,13 @@ func cleanURLPath(urlPath string) (string, error) {
func (s serveType) String() string {
switch s {
case serveTypeHTTP:
return "httpListener"
return "http"
case serveTypeHTTPS:
return "httpsListener"
return "https"
case serveTypeTCP:
return "tcpListener"
return "tcp"
case serveTypeTLSTerminatedTCP:
return "tlsTerminatedTCPListener"
return "tls-terminated-tcp"
default:
return "unknownServeType"
}

View File

@@ -697,7 +697,7 @@ func TestServeDevConfigMutations(t *testing.T) {
})
add(step{ // try to start a web handler on the same port
command: cmd("serve --https=443 --bg localhost:3000"),
wantErr: exactErr(errHelp, "errHelp"),
wantErr: anyErr(),
})
add(step{reset: true})
add(step{ // start a web handler on port 443
@@ -781,6 +781,111 @@ func TestServeDevConfigMutations(t *testing.T) {
}
}
func TestValidateConfig(t *testing.T) {
tests := [...]struct {
name string
desc string
cfg *ipn.ServeConfig
servePort uint16
serveType serveType
bg bool
wantErr bool
}{
{
name: "nil_config",
desc: "when config is nil, all requests valid",
cfg: nil,
servePort: 3000,
serveType: serveTypeHTTPS,
},
{
name: "new_bg_tcp",
desc: "no error when config exists but we're adding a new bg tcp port",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
},
bg: true,
servePort: 10000,
serveType: serveTypeHTTPS,
},
{
name: "override_bg_tcp",
desc: "no error when overwriting previous port under the same serve type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {TCPForward: "http://localhost:4545"},
},
},
bg: true,
servePort: 443,
serveType: serveTypeTCP,
},
{
name: "override_bg_tcp",
desc: "error when overwriting previous port under a different serve type",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
},
bg: true,
servePort: 443,
serveType: serveTypeHTTP,
wantErr: true,
},
{
name: "new_fg_port",
desc: "no error when serving a new foreground port",
cfg: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true},
},
Foreground: map[string]*ipn.ServeConfig{
"abc123": {
TCP: map[uint16]*ipn.TCPPortHandler{
3000: {HTTPS: true},
},
},
},
},
servePort: 4040,
serveType: serveTypeTCP,
},
{
name: "same_fg_port",
desc: "error when overwriting a previous fg port",
cfg: &ipn.ServeConfig{
Foreground: map[string]*ipn.ServeConfig{
"abc123": {
TCP: map[uint16]*ipn.TCPPortHandler{
3000: {HTTPS: true},
},
},
},
},
servePort: 3000,
serveType: serveTypeTCP,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
se := serveEnv{bg: tc.bg}
err := se.validateConfig(tc.cfg, tc.servePort, tc.serveType)
if err == nil && tc.wantErr {
t.Fatal("expected an error but got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestSrcTypeFromFlags(t *testing.T) {
tests := []struct {
name string

View File

@@ -763,7 +763,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
// queryFeatureResponse is the mock response desired from the
// call made to lc.QueryFeature by verifyFunnelEnabled.
queryFeatureResponse mockQueryFeatureResponse
caps []string // optionally set at fakeStatus.Capabilities
caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities
wantErr string
wantPanic string
}{
@@ -780,13 +780,13 @@ func TestVerifyFunnelEnabled(t *testing.T) {
{
name: "fallback-flow-missing-acl-rule",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
caps: []string{tailcfg.CapabilityHTTPS},
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS},
wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`,
},
{
name: "fallback-flow-enabled",
queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")},
caps: []string{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel},
wantErr: "", // no error, success
},
{
@@ -858,7 +858,7 @@ var fakeStatus = &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
Capabilities: []tailcfg.NodeCapability{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
},
}

View File

@@ -53,6 +53,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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/util/linuxfw
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
@@ -175,7 +176,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli
golang.org/x/exp/maps from tailscale.com/cmd/tailscale/cli+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http+

View File

@@ -289,6 +289,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/control/controlclient+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
@@ -387,7 +388,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 github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock
golang.org/x/exp/maps from tailscale.com/wgengine/magicsock+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+

View File

@@ -711,7 +711,14 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
return netstack.Create(logf, sys.Tun.Get(), sys.Engine.Get(), sys.MagicSock.Get(), sys.Dialer.Get(), sys.DNSManager.Get())
return netstack.Create(logf,
sys.Tun.Get(),
sys.Engine.Get(),
sys.MagicSock.Get(),
sys.Dialer.Get(),
sys.DNSManager.Get(),
sys.ProxyMapper(),
)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -37,6 +37,7 @@ import (
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/views"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/words"
@@ -109,7 +110,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
sys.Set(eng)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
@@ -126,7 +127,7 @@ func newIPN(jsConfig js.Value) map[string]any {
sys.NetstackRouter.Set(true)
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
srv := ipnserver.New(logf, logid, sys.NetMon.Get())
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
@@ -249,11 +250,11 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
Self: jsNetMapSelfNode{
jsNetMapNode: jsNetMapNode{
Name: nm.Name,
Addresses: mapSlice(nm.Addresses, func(a netip.Prefix) string { return a.Addr().String() }),
Addresses: mapSliceView(nm.GetAddresses(), func(a netip.Prefix) string { return a.Addr().String() }),
NodeKey: nm.NodeKey.String(),
MachineKey: nm.MachineKey.String(),
},
MachineStatus: jsMachineStatus[nm.MachineStatus],
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
},
Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
name := p.Name()
@@ -578,6 +579,14 @@ func mapSlice[T any, M any](a []T, f func(T) M) []M {
return n
}
func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
n := make([]M, a.Len())
for i := range a.LenIter() {
n[i] = f(a.At(i))
}
return n
}
func filterSlice[T any](a []T, f func(T) bool) []T {
n := make([]T, 0, len(a))
for _, e := range a {

View File

@@ -136,21 +136,29 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
dst := new(StructWithSlices)
*dst = *src
dst.Values = append(src.Values[:0:0], src.Values...)
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
for i := range dst.ValuePointers {
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
if src.ValuePointers != nil {
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
for i := range dst.ValuePointers {
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
}
}
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
for i := range dst.StructPointers {
dst.StructPointers[i] = src.StructPointers[i].Clone()
if src.StructPointers != nil {
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
for i := range dst.StructPointers {
dst.StructPointers[i] = src.StructPointers[i].Clone()
}
}
dst.Structs = make([]StructWithPtrs, len(src.Structs))
for i := range dst.Structs {
dst.Structs[i] = *src.Structs[i].Clone()
if src.Structs != nil {
dst.Structs = make([]StructWithPtrs, len(src.Structs))
for i := range dst.Structs {
dst.Structs[i] = *src.Structs[i].Clone()
}
}
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
dst.Ints[i] = ptr.To(*src.Ints[i])
if src.Ints != nil {
dst.Ints = make([]*int, len(src.Ints))
for i := range dst.Ints {
dst.Ints[i] = ptr.To(*src.Ints[i])
}
}
dst.Slice = append(src.Slice[:0:0], src.Slice...)
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)

View File

@@ -237,9 +237,10 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
slice := u
sElem := slice.Elem()
switch x := sElem.(type) {
case *types.Basic:
case *types.Basic, *types.Named:
sElem := it.QualifiedName(sElem)
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
args.MapValueType = "[]" + sElem.String()
args.MapValueType = "[]" + sElem
args.MapFn = "views.SliceOf(t)"
template = "mapFnField"
case *types.Pointer:

View File

@@ -1501,7 +1501,7 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
if err != nil {
return nil, err
}
c.logf("creating new noise client")
c.logf("[v1] creating new noise client")
nc, err := NewNoiseClient(NoiseOpts{
PrivKey: k,
ServerPubKey: serverNoiseKey,

View File

@@ -187,8 +187,9 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
if resp.Node != nil {
if DevKnob.StripCaps() {
resp.Node.Capabilities = nil
resp.Node.CapMap = nil
}
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities)
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.Capabilities, resp.Node.CapMap)
}
// Call Node.InitDisplayNames on any changed nodes.
@@ -324,6 +325,7 @@ var (
patchLastSeen = clientmetric.NewCounter("controlclient_patch_lastseen")
patchKeyExpiry = clientmetric.NewCounter("controlclient_patch_keyexpiry")
patchCapabilities = clientmetric.NewCounter("controlclient_patch_capabilities")
patchCapMap = clientmetric.NewCounter("controlclient_patch_capmap")
patchKeySignature = clientmetric.NewCounter("controlclient_patch_keysig")
patchifiedPeer = clientmetric.NewCounter("controlclient_patchified_peer")
@@ -452,6 +454,10 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
mut.KeySignature = v
patchKeySignature.Add(1)
}
if v := pc.CapMap; v != nil {
mut.CapMap = v
patchCapMap.Add(1)
}
*vp = mut.View()
}
@@ -647,6 +653,10 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if was.Cap() != n.Cap {
pc().Cap = n.Cap
}
case "CapMap":
if n.CapMap != nil {
pc().CapMap = n.CapMap
}
case "Tags":
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
return nil, false
@@ -693,6 +703,27 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if va == nil || vb == nil || *va != *vb {
return nil, false
}
case "SelfNodeV6MasqAddrForThisPeer":
va, vb := was.SelfNodeV6MasqAddrForThisPeer(), n.SelfNodeV6MasqAddrForThisPeer
if va == nil && vb == nil {
continue
}
if va == nil || vb == nil || *va != *vb {
return nil, false
}
case "ExitNodeDNSResolvers":
va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers)
if va.Len() != vb.Len() {
return nil, false
}
for i := range va.LenIter() {
if !va.At(i).Equal(vb.At(i)) {
return nil, false
}
}
}
}
if ret != nil {
@@ -739,12 +770,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
nm.SelfNode = node
nm.Expiry = node.KeyExpiry()
nm.Name = node.Name()
nm.Addresses = filterSelfAddresses(node.Addresses().AsSlice())
if node.MachineAuthorized() {
nm.MachineStatus = tailcfg.MachineAuthorized
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
}
ms.addUserProfile(nm, nm.User())

View File

@@ -20,6 +20,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -328,13 +329,13 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1,
Capabilities: ptr.To([]string{"foo"}),
Capabilities: ptr.To([]tailcfg.NodeCapability{"foo"}),
}},
},
want: peers(&tailcfg.Node{
ID: 1,
Name: "foo",
Capabilities: []string{"foo"},
Capabilities: []tailcfg.NodeCapability{"foo"},
}),
wantStats: updateStats{changed: 1},
}}
@@ -684,15 +685,15 @@ func TestPeerChangeDiff(t *testing.T) {
},
{
name: "patch-capabilities-to-nonempty",
a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}},
b: &tailcfg.Node{ID: 1, Capabilities: []string{"bar"}},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string{"bar"})},
a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}},
b: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"bar"}},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability{"bar"})},
},
{
name: "patch-capabilities-to-empty",
a: &tailcfg.Node{ID: 1, Capabilities: []string{"foo"}},
a: &tailcfg.Node{ID: 1, Capabilities: []tailcfg.NodeCapability{"foo"}},
b: &tailcfg.Node{ID: 1},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]string(nil))},
want: &tailcfg.PeerChange{NodeID: 1, Capabilities: ptr.To([]tailcfg.NodeCapability(nil))},
},
{
name: "patch-online-to-true",
@@ -735,6 +736,18 @@ func TestPeerChangeDiff(t *testing.T) {
a: &tailcfg.Node{ID: 1, User: 1},
b: &tailcfg.Node{ID: 1, User: 2},
want: nil,
},
{
name: "miss-change-masq-v4",
a: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))},
b: &tailcfg.Node{ID: 1, SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.2"))},
want: nil,
},
{
name: "miss-change-masq-v6",
a: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))},
b: &tailcfg.Node{ID: 1, SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3006"))},
want: nil,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -835,6 +848,40 @@ func TestPatchifyPeersChanged(t *testing.T) {
},
},
},
{
name: "change_exitnodednsresolvers",
mr0: &tailcfg.MapResponse{
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
Peers: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
},
},
want: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
},
},
},
{
name: "same_exitnoderesolvers",
mr0: &tailcfg.MapResponse{
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
Peers: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
},
},
want: &tailcfg.MapResponse{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -6,6 +6,7 @@
package controlknobs
import (
"slices"
"sync/atomic"
"tailscale.com/syncs"
@@ -44,43 +45,38 @@ type Knobs struct {
// incremental (delta) netmap updates and should treat all netmap
// changes as "full" ones as tailscaled did in 1.48.x and earlier.
DisableDeltaUpdates atomic.Bool
// PeerMTUEnable is whether the node should do peer path MTU discovery.
PeerMTUEnable atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
// node attributes (Node.Capabilities).
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []string) {
func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability, capMap tailcfg.NodeCapMap) {
if k == nil {
return
}
var (
keepFullWG bool
disableDRPO bool
disableUPnP bool
randomizeClientPort bool
disableDeltaUpdates bool
oneCGNAT opt.Bool
forceBackgroundSTUN bool
)
for _, attr := range selfNodeAttrs {
switch attr {
case tailcfg.NodeAttrDebugDisableWGTrim:
keepFullWG = true
case tailcfg.NodeAttrDebugDisableDRPO:
disableDRPO = true
case tailcfg.NodeAttrDisableUPnP:
disableUPnP = true
case tailcfg.NodeAttrRandomizeClientPort:
randomizeClientPort = true
case tailcfg.NodeAttrOneCGNATEnable:
oneCGNAT.Set(true)
case tailcfg.NodeAttrOneCGNATDisable:
oneCGNAT.Set(false)
case tailcfg.NodeAttrDebugForceBackgroundSTUN:
forceBackgroundSTUN = true
case tailcfg.NodeAttrDisableDeltaUpdates:
disableDeltaUpdates = true
}
has := func(attr tailcfg.NodeCapability) bool {
_, ok := capMap[attr]
return ok || slices.Contains(selfNodeAttrs, attr)
}
var (
keepFullWG = has(tailcfg.NodeAttrDebugDisableWGTrim)
disableDRPO = has(tailcfg.NodeAttrDebugDisableDRPO)
disableUPnP = has(tailcfg.NodeAttrDisableUPnP)
randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort)
disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates)
oneCGNAT opt.Bool
forceBackgroundSTUN = has(tailcfg.NodeAttrDebugForceBackgroundSTUN)
peerMTUEnable = has(tailcfg.NodeAttrPeerMTUEnable)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
oneCGNAT.Set(true)
} else if has(tailcfg.NodeAttrOneCGNATDisable) {
oneCGNAT.Set(false)
}
k.KeepFullWGConfig.Store(keepFullWG)
k.DisableDRPO.Store(disableDRPO)
k.DisableUPnP.Store(disableUPnP)
@@ -88,6 +84,7 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []string) {
k.OneCGNAT.Store(oneCGNAT)
k.ForceBackgroundSTUN.Store(forceBackgroundSTUN)
k.DisableDeltaUpdates.Store(disableDeltaUpdates)
k.PeerMTUEnable.Store(peerMTUEnable)
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
@@ -104,5 +101,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
"OneCGNAT": k.OneCGNAT.Load(),
"ForceBackgroundSTUN": k.ForceBackgroundSTUN.Load(),
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
"PeerMTUEnable": k.PeerMTUEnable.Load(),
}
}

View File

@@ -9,6 +9,7 @@ import (
"sync/atomic"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/views"
)
@@ -22,7 +23,7 @@ import (
// c2n log level changes), and via capabilities from a NetMap (so users can
// enable logging via the ACL JSON).
type LogKnob struct {
capName string
capName tailcfg.NodeCapability
cap atomic.Bool
env func() bool
manual atomic.Bool
@@ -30,7 +31,7 @@ type LogKnob struct {
// NewLogKnob creates a new LogKnob, with the provided environment variable
// name and/or NetMap capability.
func NewLogKnob(env, cap string) *LogKnob {
func NewLogKnob(env string, cap tailcfg.NodeCapability) *LogKnob {
if env == "" && cap == "" {
panic("must provide either an environment variable or capability")
}
@@ -58,7 +59,7 @@ func (lk *LogKnob) Set(v bool) {
// about; we use this rather than a concrete type to avoid a circular
// dependency.
type NetMap interface {
SelfCapabilities() views.Slice[string]
SelfCapabilities() views.Slice[tailcfg.NodeCapability]
}
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap

View File

@@ -64,7 +64,7 @@ func TestLogKnob(t *testing.T) {
testKnob.UpdateFromNetMap(&netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
Capabilities: []string{
Capabilities: []tailcfg.NodeCapability{
"https://tailscale.com/cap/testing",
},
}).View(),

View File

@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-TZP/FQqb21yiKMlIPXXSoN6HfiBAun+gPZHQ5cPc8L0=
# nix-direnv cache busting line: sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=

3
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/evanw/esbuild v0.14.53
github.com/frankban/quicktest v1.14.5
github.com/fxamacker/cbor/v2 v2.4.0
github.com/go-json-experiment/json v0.0.0-20230321051131-ccbac49a6929
github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37
github.com/go-logr/zapr v1.2.4
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
@@ -65,6 +65,7 @@ require (
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109
github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8
github.com/tc-hib/winres v0.2.0
github.com/tcnksm/go-httpstat v0.2.0

View File

@@ -1 +1 @@
sha256-TZP/FQqb21yiKMlIPXXSoN6HfiBAun+gPZHQ5cPc8L0=
sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=

6
go.sum
View File

@@ -299,8 +299,8 @@ github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhc
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20230321051131-ccbac49a6929 h1:GdbUZo0+623j+pKRhwwdf1q28IUgRc7asx3TjF9b7VQ=
github.com/go-json-experiment/json v0.0.0-20230321051131-ccbac49a6929/go.mod h1:AHV+bpNGVGD0DCHMBhhTYtT7yeBYD9Yk92XAjB7vOgo=
github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37 h1:JFXgl/SzxYP9ULDFS97xkKB/Bfdywrc3rre1nYEwMj8=
github.com/go-json-experiment/json v0.0.0-20230908182459-f320be06fe37/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -881,6 +881,8 @@ github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109 h1:QPRZXpvopDySnmNobTe7Dyc/w6ULt1uCN+3/9cTJwjo=
github.com/tailscale/web-client-prebuilt v0.0.0-20230919163828-68bd39ee4109/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8 h1:V9kSpiTzFp7OTgJinu/kSJlsI6EfRs8wJgQ+Q+5a8v4=
github.com/tailscale/wireguard-go v0.0.0-20230824215414-93bd5cbf7fd8/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw=
github.com/tc-hib/winres v0.2.0 h1:gly/ivDWGvlhl7ENtEmA7wPQ6dWab1LlLq/DgcZECKE=

View File

@@ -1 +1 @@
27f103a44f8fd34a2cc36995ce7bf83d04433ead
2071f43f327a8d544cd2df4b19398ed681e825c7

View File

@@ -91,6 +91,7 @@ var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
Web map[HostPort]*WebServerConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// Clone makes a deep copy of TCPPortHandler.

View File

@@ -182,6 +182,7 @@ func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeCon
return t.View()
})
}
func (v ServeConfigView) ETag() string { return v.ж.ETag }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
@@ -189,6 +190,7 @@ var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
Web map[HostPort]*WebServerConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// View returns a readonly view of TCPPortHandler.

View File

@@ -50,6 +50,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
peers []tailcfg.NodeView
os string // version.OS value; empty means linux
cloud cloudenv.Cloud
prefs *ipn.Prefs
@@ -68,23 +69,28 @@ func TestDNSConfigForNetmap(t *testing.T) {
{
name: "self_name_and_peers",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("100.101.101.101"),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
{
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
@@ -102,23 +108,28 @@ func TestDNSConfigForNetmap(t *testing.T) {
// even if they have IPv4.
name: "v6_only_self",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("fe75::1"),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
},
{
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("fe75::1"),
}).View(),
},
peers: nodeViews([]*tailcfg.Node{
{
ID: 1,
Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"),
},
{
ID: 2,
Name: "b.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::2"),
},
{
ID: 3,
Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
},
}),
prefs: &ipn.Prefs{},
want: &dns.Config{
OnlyIPv6: true,
@@ -134,8 +145,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
{
name: "extra_records",
nm: &netmap.NetworkMap{
Name: "myname.net",
Addresses: ipps("100.101.101.101"),
Name: "myname.net",
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
}).View(),
DNS: tailcfg.DNSConfig{
ExtraRecords: []tailcfg.DNSRecord{
{Name: "foo.com", Value: "1.2.3.4"},
@@ -319,7 +332,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
verOS := cmpx.Or(tt.os, "linux")
var log tstest.MemLogger
got := dnsConfigForNetmap(tt.nm, tt.prefs.View(), log.Logf, verOS)
got := dnsConfigForNetmap(tt.nm, peersMap(tt.peers), tt.prefs.View(), log.Logf, verOS)
if !reflect.DeepEqual(got, tt.want) {
gotj, _ := json.MarshalIndent(got, "", "\t")
wantj, _ := json.MarshalIndent(tt.want, "", "\t")
@@ -332,6 +345,17 @@ func TestDNSConfigForNetmap(t *testing.T) {
}
}
func peersMap(s []tailcfg.NodeView) map[tailcfg.NodeID]tailcfg.NodeView {
m := make(map[tailcfg.NodeID]tailcfg.NodeView)
for _, n := range s {
if n.ID() == 0 {
panic("zero Node.ID")
}
m[n.ID()] = n
}
return m
}
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
b := &LocalBackend{}
if b.allowExitNodeDNSProxyToServeName("google.com") {

View File

@@ -31,6 +31,7 @@ import (
"go4.org/mem"
"go4.org/netipx"
xmaps "golang.org/x/exp/maps"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
@@ -203,11 +204,21 @@ type LocalBackend struct {
capFileSharing bool // whether netMap contains the file sharing capability
capTailnetLock bool // whether netMap contains the tailnet lock capability
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
netMap *netmap.NetworkMap // not mutated in place once set (except for Peers slice)
hostinfo *tailcfg.Hostinfo
// netMap is the most recently set full netmap from the controlclient.
// It can't be mutated in place once set. Because it can't be mutated in place,
// delta updates from the control server don't apply to it. Instead, use
// the peers map to get up-to-date information on the state of peers.
// In general, avoid using the netMap.Peers slice. We'd like it to go away
// as of 2023-09-17.
netMap *netmap.NetworkMap
// peers is the set of current peers and their current values after applying
// delta node mutations as they come in (with mu held). The map values can
// be given out to callers, but the map itself must not escape the LocalBackend.
peers map[tailcfg.NodeID]tailcfg.NodeView
nodeByAddr map[netip.Addr]tailcfg.NodeID
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil
nodeByAddr map[netip.Addr]tailcfg.NodeView
activeLogin string // last logged LoginName from netMap
activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint
blocked bool
@@ -541,7 +552,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs())
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := len(b.netMap.Addresses)
want := b.netMap.GetAddresses().Len()
if len(b.peerAPIListeners) < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
go b.initPeerAPIListener()
@@ -650,18 +661,8 @@ func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
// UpdateStatus implements ipnstate.StatusUpdater.
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
b.e.UpdateStatus(sb)
var extraLocked func(*ipnstate.StatusBuilder)
if sb.WantPeers {
extraLocked = b.populatePeerStatusLocked
}
b.updateStatus(sb, extraLocked)
}
b.e.UpdateStatus(sb) // does wireguard + magicsock status
// updateStatus populates sb with status.
//
// extraLocked, if non-nil, is called while b.mu is still held.
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -721,8 +722,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
var tailscaleIPs []netip.Addr
if b.netMap != nil {
for _, addr := range b.netMap.Addresses {
if addr.IsSingleIP() {
addrs := b.netMap.GetAddresses()
for i := range addrs.LenIter() {
if addr := addrs.At(i); addr.IsSingleIP() {
sb.AddTailscaleIP(addr.Addr())
tailscaleIPs = append(tailscaleIPs, addr.Addr())
}
@@ -744,6 +746,13 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
if c := sn.Capabilities(); c.Len() > 0 {
ss.Capabilities = c.AsSlice()
}
if cm := sn.CapMap(); cm.Len() > 0 {
ss.CapMap = make(tailcfg.NodeCapMap, sn.CapMap().Len())
cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
ss.CapMap[k] = v.AsSlice()
return true
})
}
}
for _, addr := range tailscaleIPs {
ss.TailscaleIPs = append(ss.TailscaleIPs, addr)
@@ -759,8 +768,8 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
// TODO: hostinfo, and its networkinfo
// TODO: EngineStatus copy (and deprecate it?)
if extraLocked != nil {
extraLocked(sb)
if sb.WantPeers {
b.populatePeerStatusLocked(sb)
}
}
@@ -772,7 +781,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
sb.AddUser(id, up)
}
exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
for _, p := range b.netMap.Peers {
for _, p := range b.peers {
var lastSeen time.Time
if p.LastSeen() != nil {
lastSeen = *p.LastSeen()
@@ -845,20 +854,31 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.
var zero tailcfg.NodeView
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ipp.Addr()]
nid, ok := b.nodeByAddr[ipp.Addr()]
if !ok {
var ip netip.Addr
if ipp.Port() != 0 {
ip, ok = b.e.WhoIsIPPort(ipp)
ip, ok = b.sys.ProxyMapper().WhoIsIPPort(ipp)
}
if !ok {
return zero, u, false
}
n, ok = b.nodeByAddr[ip]
nid, ok = b.nodeByAddr[ip]
if !ok {
return zero, u, false
}
}
if b.netMap == nil {
return zero, u, false
}
n, ok = b.peers[nid]
if !ok {
// Check if this the self-node, which would not appear in peers.
if !b.netMap.SelfNode.Valid() || nid != b.netMap.SelfNode.ID() {
return zero, u, false
}
n = b.netMap.SelfNode
}
u, ok = b.netMap.UserProfiles[n.User()]
if !ok {
return zero, u, false
@@ -882,7 +902,9 @@ func (b *LocalBackend) peerCapsLocked(src netip.Addr) tailcfg.PeerCapMap {
if filt == nil {
return nil
}
for _, a := range b.netMap.Addresses {
addrs := b.netMap.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
if !a.IsSingleIP() {
continue
}
@@ -1030,7 +1052,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
// Perform all mutations of prefs based on the netmap here.
if prefsChanged {
// Prefs will be written out if stale; this is not safe unless locked or cloned.
if err := b.pm.SetPrefs(prefs.View()); err != nil {
if err := b.pm.SetPrefs(prefs.View(), st.NetMap.MagicDNSSuffix()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
@@ -1081,7 +1103,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.mu.Lock()
prefs.WantRunning = false
p := prefs.View()
if err := b.pm.SetPrefs(p); err != nil {
if err := b.pm.SetPrefs(p, st.NetMap.MagicDNSSuffix()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
b.mu.Unlock()
@@ -1125,40 +1147,79 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
return false
}
var notify *ipn.Notify // non-nil if we need to send a Notify
defer func() {
if notify != nil {
b.send(*notify)
}
}()
b.mu.Lock()
defer b.mu.Unlock()
return b.updateNetmapDeltaLocked(muts)
if !b.updateNetmapDeltaLocked(muts) {
return false
}
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
nm := ptr.To(*b.netMap) // shallow clone
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
for _, p := range b.peers {
nm.Peers = append(nm.Peers, p)
}
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
})
notify = &ipn.Notify{NetMap: nm}
} else if testenv.InTest() {
// In tests, send an empty Notify as a wake-up so end-to-end
// integration tests in another repo can check on the status of
// LocalBackend after processing deltas.
notify = new(ipn.Notify)
}
return true
}
// mutationsAreWorthyOfTellingIPNBus reports whether any mutation type in muts is
// worthy of spamming the IPN bus (the Windows & Mac GUIs, basically) to tell them
// about the update.
func mutationsAreWorthyOfTellingIPNBus(muts []netmap.NodeMutation) bool {
for _, m := range muts {
switch m.(type) {
case netmap.NodeMutationLastSeen,
netmap.NodeMutationOnline:
// The GUI clients might render peers differently depending on whether
// they're online.
return true
}
}
return false
}
func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (handled bool) {
if b.netMap == nil {
if b.netMap == nil || len(b.peers) == 0 {
return false
}
peers := b.netMap.Peers
// Locally cloned mutable nodes, to avoid calling AsStruct (clone)
// multiple times on a node if it's mutated multiple times in this
// call (e.g. its endpoints + online status both change)
var mutableNodes map[tailcfg.NodeID]*tailcfg.Node
for _, m := range muts {
// LocalBackend only cares about some types of mutations.
// (magicsock cares about different ones.)
switch m.(type) {
case netmap.NodeMutationOnline, netmap.NodeMutationLastSeen:
default:
continue
n, ok := mutableNodes[m.NodeIDBeingMutated()]
if !ok {
nv, ok := b.peers[m.NodeIDBeingMutated()]
if !ok {
// TODO(bradfitz): unexpected metric?
return false
}
n = nv.AsStruct()
mak.Set(&mutableNodes, nv.ID(), n)
}
nodeID := m.NodeIDBeingMutated()
idx := b.netMap.PeerIndexByNodeID(nodeID)
if idx == -1 {
continue
}
mut := peers[idx].AsStruct()
switch m := m.(type) {
case netmap.NodeMutationOnline:
mut.Online = ptr.To(m.Online)
case netmap.NodeMutationLastSeen:
mut.LastSeen = ptr.To(m.LastSeen)
}
peers[idx] = mut.View()
m.Apply(n)
}
for nid, n := range mutableNodes {
b.peers[nid] = n.View()
}
return true
}
@@ -1289,6 +1350,27 @@ func (b *LocalBackend) SetControlClientGetterForTesting(newControlClient func(co
b.ccGen = newControlClient
}
// NodeViewByIDForTest returns the state of the node with the given ID
// for integration tests in another repo.
func (b *LocalBackend) NodeViewByIDForTest(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok := b.peers[id]
return n, ok
}
// PeersForTest returns all the current peers, sorted by Node.ID,
// for integration tests in another repo.
func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
b.mu.Lock()
defer b.mu.Unlock()
ret := xmaps.Values(b.peers)
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
})
return ret
}
func (b *LocalBackend) getNewControlClientFunc() clientGen {
b.mu.Lock()
defer b.mu.Unlock()
@@ -1416,7 +1498,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
newPrefs := opts.UpdatePrefs.Clone()
newPrefs.Persist = oldPrefs.Persist().AsStruct()
pv := newPrefs.View()
if err := b.pm.SetPrefs(pv); err != nil {
if err := b.pm.SetPrefs(pv, b.netMap.MagicDNSSuffix()); err != nil {
b.logf("failed to save UpdatePrefs state: %v", err)
}
b.setAtomicValuesFromPrefsLocked(pv)
@@ -1576,7 +1658,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
// quite hard to debug, so save yourself the trouble.
var (
haveNetmap = netMap != nil
addrs []netip.Prefix
addrs views.Slice[netip.Prefix]
packetFilter []filter.Match
localNetsB netipx.IPSetBuilder
logNetsB netipx.IPSetBuilder
@@ -1587,13 +1669,13 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
logNetsB.AddPrefix(tsaddr.TailscaleULARange())
logNetsB.RemovePrefix(tsaddr.ChromeOSVMRange())
if haveNetmap {
addrs = netMap.Addresses
for _, p := range addrs {
localNetsB.AddPrefix(p)
addrs = netMap.GetAddresses()
for i := range addrs.LenIter() {
localNetsB.AddPrefix(addrs.At(i))
}
packetFilter = netMap.PacketFilter
if packetFilterPermitsUnlockedNodes(netMap.Peers, packetFilter) {
if packetFilterPermitsUnlockedNodes(b.peers, packetFilter) {
err := errors.New("server sent invalid packet filter permitting traffic to unlocked nodes; rejecting all packets for safety")
warnInvalidUnsignedNodes.Set(err)
packetFilter = nil
@@ -1641,7 +1723,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
changed := deephash.Update(&b.filterHash, &struct {
HaveNetmap bool
Addrs []netip.Prefix
Addrs views.Slice[netip.Prefix]
FilterMatch []filter.Match
LocalNets []netipx.IPRange
LogNets []netipx.IPRange
@@ -1678,7 +1760,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
//
// If this reports true, the packet filter is invalid (the server is either broken
// or malicious) and should be ignored for safety.
func packetFilterPermitsUnlockedNodes(peers []tailcfg.NodeView, packetFilter []filter.Match) bool {
func packetFilterPermitsUnlockedNodes(peers map[tailcfg.NodeID]tailcfg.NodeView, packetFilter []filter.Match) bool {
var b netipx.IPSetBuilder
var numUnlocked int
for _, p := range peers {
@@ -1843,53 +1925,6 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet,
return b.IPSet()
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
// for DNS map construction purposes (that is, only the first entry counts).
func dnsCIDRsEqual(newAddr, oldAddr views.Slice[netip.Prefix]) bool {
if newAddr.Len() != oldAddr.Len() {
return false
}
if newAddr.Len() == 0 || newAddr.At(0) == oldAddr.At(0) {
return true
}
return false
}
// dnsMapsEqual determines whether the new and the old network map
// induce the same DNS map. It does so without allocating memory,
// at the expense of giving false negatives if peers are reordered.
func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
if (old == nil) != (new == nil) {
return false
}
if old == nil && new == nil {
return true
}
if len(new.Peers) != len(old.Peers) {
return false
}
if new.Name != old.Name {
return false
}
if !dnsCIDRsEqual(views.SliceOf(new.Addresses), views.SliceOf(old.Addresses)) {
return false
}
for i, newPeer := range new.Peers {
oldPeer := old.Peers[i]
if newPeer.Name() != oldPeer.Name() {
return false
}
if !dnsCIDRsEqual(newPeer.Addresses(), oldPeer.Addresses()) {
return false
}
}
return true
}
// readPoller is a goroutine that receives service lists from
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
@@ -2098,6 +2133,23 @@ func (b *LocalBackend) DebugNotify(n ipn.Notify) {
b.send(n)
}
// DebugForceNetmapUpdate forces a full no-op netmap update of the current
// netmap in all the various subsystems (wireguard, magicsock, LocalBackend).
//
// It exists for load testing reasons (for issue 1909), doing what would happen
// if a new MapResponse came in from the control server that couldn't be handled
// incrementally.
func (b *LocalBackend) DebugForceNetmapUpdate() {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.netMap
b.e.SetNetworkMap(nm)
if nm != nil {
b.magicConn().SetDERPMap(nm.DERPMap)
}
b.setNetMapLocked(nm)
}
// send delivers n to the connected frontend and any API watchers from
// LocalBackend.WatchNotifications (via the LocalAPI).
//
@@ -2337,7 +2389,7 @@ func (b *LocalBackend) migrateStateLocked(prefs *ipn.Prefs) (err error) {
// Backend owns the state, but frontend is trying to migrate
// state into the backend.
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
if err := b.pm.SetPrefs(prefs.View()); err != nil {
if err := b.pm.SetPrefs(prefs.View(), b.netMap.MagicDNSSuffix()); err != nil {
return fmt.Errorf("store.WriteState: %v", err)
}
}
@@ -2390,13 +2442,13 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD())
if !p.Valid() {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
b.containsViaIPFuncAtomic.Store(tsaddr.FalseContainsIPFunc())
b.setTCPPortsIntercepted(nil)
b.lastServeConfJSON = mem.B(nil)
b.serveConfig = ipn.ServeConfigView{}
} else {
filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix)
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered))
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(views.SliceOf(filtered)))
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
}
}
@@ -2885,7 +2937,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn
}
prefs := newp.View()
if err := b.pm.SetPrefs(prefs); err != nil {
if err := b.pm.SetPrefs(prefs, b.netMap.MagicDNSSuffix()); err != nil {
b.logf("failed to save new controlclient state: %v", err)
}
b.lastProfileID = b.pm.CurrentProfile().ID
@@ -2950,7 +3002,7 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
nm := b.NetMap()
return nm != nil && slices.Contains(nm.Addresses, netip.PrefixFrom(ip, ip.BitLen()))
return nm != nil && views.SliceContains(nm.GetAddresses(), netip.PrefixFrom(ip, ip.BitLen()))
}
var (
@@ -3084,6 +3136,8 @@ func (b *LocalBackend) authReconfig() {
nm := b.netMap
hasPAC := b.prevIfState.HasPAC()
disableSubnetsIfPAC := hasCapability(nm, tailcfg.NodeAttrDisableSubnetsIfPAC)
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS())
b.mu.Unlock()
if blocked {
@@ -3116,7 +3170,7 @@ func (b *LocalBackend) authReconfig() {
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
if dohURLOK {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
@@ -3130,7 +3184,6 @@ func (b *LocalBackend) authReconfig() {
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.ControlKnobs(), version.OS())
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
dcfg := dnsConfigForNetmap(nm, prefs, b.logf, version.OS())
err = b.e.Reconfig(cfg, rcfg, dcfg)
if err == wgengine.ErrNoChanges {
@@ -3179,15 +3232,18 @@ func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs,
//
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
// a runtime.GOOS.
func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config {
func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config {
if nm == nil {
return nil
}
dcfg := &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{},
}
// selfV6Only is whether we only have IPv6 addresses ourselves.
selfV6Only := slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
!slices.ContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
selfV6Only := views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs6) &&
!views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs4)
dcfg.OnlyIPv6 = selfV6Only
// Populate MagicDNS records. We do this unconditionally so that
@@ -3234,8 +3290,8 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
}
dcfg.Hosts[fqdn] = ips
}
set(nm.Name, views.SliceOf(nm.Addresses))
for _, peer := range nm.Peers {
set(nm.Name, nm.GetAddresses())
for _, peer := range peers {
set(peer.Name(), peer.Addresses())
}
for _, rec := range nm.DNS.ExtraRecords {
@@ -3283,7 +3339,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it.
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
if dohURL, ok := exitNodeCanProxyDNS(nm, peers, prefs.ExitNodeID()); ok {
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
return dcfg
}
@@ -3449,10 +3505,11 @@ func (b *LocalBackend) initPeerAPIListener() {
return
}
if len(b.netMap.Addresses) == len(b.peerAPIListeners) {
addrs := b.netMap.GetAddresses()
if addrs.Len() == len(b.peerAPIListeners) {
allSame := true
for i, pln := range b.peerAPIListeners {
if pln.ip != b.netMap.Addresses[i].Addr() {
if pln.ip != addrs.At(i).Addr() {
allSame = false
break
}
@@ -3466,7 +3523,7 @@ func (b *LocalBackend) initPeerAPIListener() {
b.closePeerAPIListenersLocked()
selfNode := b.netMap.SelfNode
if len(b.netMap.Addresses) == 0 || !selfNode.Valid() {
if !selfNode.Valid() || b.netMap.GetAddresses().Len() == 0 {
return
}
@@ -3487,7 +3544,8 @@ func (b *LocalBackend) initPeerAPIListener() {
b.peerAPIServer = ps
isNetstack := b.sys.IsNetstack()
for i, a := range b.netMap.Addresses {
for i := range addrs.LenIter() {
a := addrs.At(i)
var ln net.Listener
var err error
skipListen := i > 0 && isNetstack
@@ -3771,11 +3829,12 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
// Needed so that UpdateEndpoints can run
b.e.RequestStatus()
case ipn.Running:
var addrs []string
for _, addr := range netMap.Addresses {
addrs = append(addrs, addr.Addr().String())
var addrStrs []string
addrs := netMap.GetAddresses()
for i := range addrs.LenIter() {
addrStrs = append(addrStrs, addrs.At(i).Addr().String())
}
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrStrs, " "))
case ipn.NoState:
// Do nothing.
default:
@@ -3862,7 +3921,7 @@ func (b *LocalBackend) nextStateLocked() ipn.State {
// NetMap must be non-nil for us to get here.
// The node key expired, need to relogin.
return ipn.NeedsLogin
case netMap.MachineStatus != tailcfg.MachineAuthorized:
case netMap.GetMachineStatus() != tailcfg.MachineAuthorized:
// TODO(crawshaw): handle tailcfg.MachineInvalid
return ipn.NeedsMachineAuth
case state == ipn.NeedsMachineAuth:
@@ -4058,9 +4117,9 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
cc.SetNetInfo(ni)
}
func hasCapability(nm *netmap.NetworkMap, cap string) bool {
if nm != nil && nm.SelfNode.Valid() {
return views.SliceContains(nm.SelfNode.Capabilities(), cap)
func hasCapability(nm *netmap.NetworkMap, cap tailcfg.NodeCapability) bool {
if nm != nil {
return nm.SelfNode.HasCap(cap)
}
return false
}
@@ -4078,6 +4137,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
login = cmpx.Or(nm.UserProfiles[nm.User()].LoginName, "<missing-profile>")
}
b.netMap = nm
b.updatePeersFromNetmapLocked(nm)
if login != b.activeLogin {
b.logf("active login: %v", login)
b.activeLogin = login
@@ -4112,16 +4172,16 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
// Update the nodeByAddr index.
if b.nodeByAddr == nil {
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeID{}
}
// First pass, mark everything unwanted.
for k := range b.nodeByAddr {
b.nodeByAddr[k] = tailcfg.NodeView{}
b.nodeByAddr[k] = 0
}
addNode := func(n tailcfg.NodeView) {
for i := range n.Addresses().LenIter() {
if ipp := n.Addresses().At(i); ipp.IsSingleIP() {
b.nodeByAddr[ipp.Addr()] = n
b.nodeByAddr[ipp.Addr()] = n.ID()
}
}
}
@@ -4133,12 +4193,33 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
// Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr {
if !v.Valid() {
if v == 0 {
delete(b.nodeByAddr, k)
}
}
}
func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
if nm == nil {
b.peers = nil
return
}
// First pass, mark everything unwanted.
for k := range b.peers {
b.peers[k] = tailcfg.NodeView{}
}
// Second pass, add everything wanted.
for _, p := range nm.Peers {
mak.Set(&b.peers, p.ID(), p)
}
// Third pass, remove deleted things.
for k, v := range b.peers {
if !v.Valid() {
delete(b.peers, k)
}
}
}
// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
// capabilities in the provided NetMap.
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
@@ -4412,7 +4493,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
if !b.capFileSharing {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
for _, p := range nm.Peers {
for _, p := range b.peers {
if !b.peerIsTaildropTargetLocked(p) {
continue
}
@@ -4425,7 +4506,9 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
PeerAPIURL: peerAPI,
})
}
// TODO: sort a different way than the netmap already is?
slices.SortFunc(ret, func(a, b *apitype.FileTarget) int {
return cmpx.Compare(a.Node.Name, b.Node.Name)
})
return ret, nil
}
@@ -4536,7 +4619,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
}
var have4, have6 bool
for _, a := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
if !a.IsSingleIP() {
continue
}
@@ -4664,11 +4749,11 @@ func (b *LocalBackend) SetExpirySooner(ctx context.Context, expiry time.Time) er
// to exitNodeID's DoH service, if available.
//
// If exitNodeID is the zero valid, it returns "", false.
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.NodeView, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
if exitNodeID.IsZero() {
return "", false
}
for _, p := range nm.Peers {
for _, p := range peers {
if p.StableID() == exitNodeID && peerCanProxyDNS(p) {
return peerAPIBase(nm, p) + "/dns-query", true
}
@@ -4864,13 +4949,14 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
io.WriteString(w, "No netmap.\n")
return
}
if len(b.netMap.Addresses) == 0 {
addrs := b.netMap.GetAddresses()
if addrs.Len() == 0 {
io.WriteString(w, "No local addresses.\n")
return
}
io.WriteString(w, "<p>Local addresses:</p><ul>\n")
for _, ipp := range b.netMap.Addresses {
fmt.Fprintf(w, "<li>%v</li>\n", ipp.Addr())
for i := range addrs.LenIter() {
fmt.Fprintf(w, "<li>%v</li>\n", addrs.At(i).Addr())
}
io.WriteString(w, "</ul>\n")
}

View File

@@ -33,113 +33,6 @@ import (
"tailscale.com/wgengine/wgcfg"
)
func TestNetworkMapCompare(t *testing.T) {
prefix1, err := netip.ParsePrefix("192.168.0.0/24")
if err != nil {
t.Fatal(err)
}
node1 := &tailcfg.Node{Addresses: []netip.Prefix{prefix1}}
prefix2, err := netip.ParsePrefix("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}
node2 := &tailcfg.Node{Addresses: []netip.Prefix{prefix2}}
tests := []struct {
name string
a, b *netmap.NetworkMap
want bool
}{
{
"both nil",
nil,
nil,
true,
},
{
"b nil",
&netmap.NetworkMap{},
nil,
false,
},
{
"a nil",
nil,
&netmap.NetworkMap{},
false,
},
{
"both default",
&netmap.NetworkMap{},
&netmap.NetworkMap{},
true,
},
{
"names identical",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map1"},
true,
},
{
"names differ",
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map2"},
false,
},
{
"Peers identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
true,
},
{
"Peer list length",
// length of Peers list differs
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
false,
},
{
"Node names identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
true,
},
{
"Node names differ",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "B"}})},
false,
},
{
"Node lists identical",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
true,
},
{
"Node lists differ",
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node2})},
false,
},
{
"Node Users differ",
// User field is not checked.
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 0}})},
&netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 1}})},
true,
},
}
for _, tt := range tests {
got := dnsMapsEqual(tt.a, tt.b)
if got != tt.want {
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
}
}
}
func inRemove(ip netip.Addr) bool {
for _, pfx := range removeFromDefaultRoute {
if pfx.Contains(ip) {
@@ -385,9 +278,11 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_only_4_them_both",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -406,9 +301,11 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_only_6_them_both",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -427,10 +324,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_only_4",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -448,10 +347,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_only_6",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -469,10 +370,12 @@ func TestPeerAPIBase(t *testing.T) {
{
name: "self_both_them_no_peerapi_service",
nm: &netmap.NetworkMap{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"),
},
}).View(),
},
peer: &tailcfg.Node{
Addresses: []netip.Prefix{
@@ -499,10 +402,7 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
panic("unexpected HTTP request")
}
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
tstest.Replace(t, &panicOnMachineKeyGeneration, func() bool { return true })
func newTestLocalBackend(t testing.TB) *LocalBackend {
var logf logger.Logf = logger.Discard
sys := new(tsd.System)
store := new(mem.Store)
@@ -517,7 +417,14 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
return lb
}
// Issue 1573: don't generate a machine key if we don't want to be running.
func TestLazyMachineKeyGeneration(t *testing.T) {
tstest.Replace(t, &panicOnMachineKeyGeneration, func() bool { return true })
lb := newTestLocalBackend(t)
lb.SetHTTPTestClient(&http.Client{
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
})
@@ -760,7 +667,7 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := packetFilterPermitsUnlockedNodes(nodeViews(tt.peers), tt.filter); got != tt.want {
if got := packetFilterPermitsUnlockedNodes(peersMap(nodeViews(tt.peers)), tt.filter); got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
@@ -769,21 +676,8 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
}
func TestStatusWithoutPeers(t *testing.T) {
logf := tstest.WhileTestRunningLogger(t)
store := new(testStateStorage)
sys := new(tsd.System)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
sys.Set(e)
t.Cleanup(e.Close)
b := newTestLocalBackend(t)
b, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
}
var cc *mockControl
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc = newClient(t, opts)
@@ -795,10 +689,9 @@ func TestStatusWithoutPeers(t *testing.T) {
b.Start(ipn.Options{})
b.Login(nil)
cc.send(nil, "", false, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
Addresses: ipps("100.101.101.101"),
SelfNode: (&tailcfg.Node{
Addresses: ipps("100.101.101.101"),
MachineAuthorized: true,
Addresses: ipps("100.101.101.101"),
}).View(),
})
got := b.StatusWithoutPeers()
@@ -892,6 +785,7 @@ func TestUpdateNetmapDelta(t *testing.T) {
for i := 0; i < 5; i++ {
b.netMap.Peers = append(b.netMap.Peers, (&tailcfg.Node{ID: (tailcfg.NodeID(i) + 1)}).View())
}
b.updatePeersFromNetmapLocked(b.netMap)
someTime := time.Unix(123, 0)
muts, ok := netmap.MutationsFromMapResponse(&tailcfg.MapResponse{
@@ -925,7 +819,7 @@ func TestUpdateNetmapDelta(t *testing.T) {
wants := []*tailcfg.Node{
{
ID: 1,
DERP: "", // unmodified by the delta
DERP: "127.3.3.40:1",
},
{
ID: 2,
@@ -941,14 +835,67 @@ func TestUpdateNetmapDelta(t *testing.T) {
},
}
for _, want := range wants {
idx := b.netMap.PeerIndexByNodeID(want.ID)
if idx == -1 {
t.Errorf("ID %v not found in netmap", want.ID)
gotv, ok := b.peers[want.ID]
if !ok {
t.Errorf("netmap.Peer %v missing from b.peers", want.ID)
continue
}
got := b.netMap.Peers[idx].AsStruct()
got := gotv.AsStruct()
if !reflect.DeepEqual(got, want) {
t.Errorf("netmap.Peer %v wrong.\n got: %v\nwant: %v", want.ID, logger.AsJSON(got), logger.AsJSON(want))
}
}
}
// tests WhoIs and indirectly that setNetMapLocked updates b.nodeByAddr correctly.
func TestWhoIs(t *testing.T) {
b := newTestLocalBackend(t)
b.setNetMapLocked(&netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
ID: 1,
User: 10,
Addresses: []netip.Prefix{netip.MustParsePrefix("100.101.102.103/32")},
}).View(),
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
ID: 2,
User: 20,
Addresses: []netip.Prefix{netip.MustParsePrefix("100.200.200.200/32")},
}).View(),
},
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
10: {
DisplayName: "Myself",
},
20: {
DisplayName: "Peer",
},
},
})
tests := []struct {
q string
want tailcfg.NodeID // 0 means want ok=false
wantName string
}{
{"100.101.102.103:0", 1, "Myself"},
{"100.101.102.103:123", 1, "Myself"},
{"100.200.200.200:0", 2, "Peer"},
{"100.200.200.200:123", 2, "Peer"},
{"100.4.0.4:404", 0, ""},
}
for _, tt := range tests {
t.Run(tt.q, func(t *testing.T) {
nv, up, ok := b.WhoIs(netip.MustParseAddrPort(tt.q))
var got tailcfg.NodeID
if ok {
got = nv.ID()
}
if got != tt.want {
t.Errorf("got nodeID %v; want %v", got, tt.want)
}
if up.DisplayName != tt.wantName {
t.Errorf("got name %q; want %q", up.DisplayName, tt.wantName)
}
})
}
}

View File

@@ -578,7 +578,7 @@ func (b *LocalBackend) NetworkLockForceLocalDisable() error {
newPrefs := b.pm.CurrentPrefs().AsStruct().Clone() // .Persist should always be initialized here.
newPrefs.Persist.DisallowedTKAStateIDs = append(newPrefs.Persist.DisallowedTKAStateIDs, stateID)
if err := b.pm.SetPrefs(newPrefs.View()); err != nil {
if err := b.pm.SetPrefs(newPrefs.View(), b.netMap.MagicDNSSuffix()); err != nil {
return fmt.Errorf("saving prefs: %w", err)
}

View File

@@ -151,7 +151,7 @@ func TestTKAEnablementFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
b := LocalBackend{
capTailnetLock: true,
varRoot: temp,
@@ -191,7 +191,7 @@ func TestTKADisablementFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -383,7 +383,7 @@ func TestTKASync(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Setup the tka authority on the control plane.
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
@@ -605,7 +605,7 @@ func TestTKADisable(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -696,7 +696,7 @@ func TestTKASign(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -785,7 +785,7 @@ func TestTKAForceDisable(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
@@ -880,7 +880,7 @@ func TestTKAAffectedSigs(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -1013,7 +1013,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
}).View(), ""))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
@@ -1104,7 +1104,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
PrivateNodeKey: nodePriv,
NetworkLockKey: cosignPriv,
},
}).View()))
}).View(), ""))
b := LocalBackend{
varRoot: temp,
logf: t.Logf,

View File

@@ -136,6 +136,9 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
return "", false
}
}
if !filepath.IsLocal(baseName) {
return "", false
}
return filepath.Join(s.rootDir, baseName), true
}
@@ -612,6 +615,9 @@ func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
return *v == addr
}
if v := h.peerNode.SelfNodeV6MasqAddrForThisPeer(); v != nil {
return *v == addr
}
pfx := netip.PrefixFrom(addr, addr.BitLen())
return views.SliceContains(h.selfNode.Addresses(), pfx)
}
@@ -1035,7 +1041,7 @@ func (h *peerAPIHandler) canPutFile() bool {
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
func (h *peerAPIHandler) canDebug() bool {
if !views.SliceContains(h.selfNode.Capabilities(), tailcfg.CapabilityDebug) {
if !h.selfNode.HasCap(tailcfg.CapabilityDebug) {
// This node does not expose debug info.
return false
}

View File

@@ -660,7 +660,7 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::/0"),
},
}).View())
}).View(), "")
if !h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly not offering exit node")
}

View File

@@ -206,7 +206,12 @@ func init() {
// SetPrefs sets the current profile's prefs to the provided value.
// It also saves the prefs to the StateStore. It stores a copy of the
// provided prefs, which may be accessed via CurrentPrefs.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
//
// If tailnetMagicDNSName is provided non-empty, it will be used to
// enrich the profile with the tailnet's MagicDNS name. The MagicDNS
// name cannot be pulled from prefsIn directly because it is not saved
// on ipn.Prefs (since it's not a field that is configurable by nodes).
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, tailnetMagicDNSName string) error {
prefs := prefsIn.AsStruct()
newPersist := prefs.Persist
if newPersist == nil || newPersist.NodeID == "" || newPersist.UserProfile.LoginName == "" {
@@ -250,6 +255,9 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
cp.ControlURL = prefs.ControlURL
cp.UserProfile = newPersist.UserProfile
cp.NodeID = newPersist.NodeID
if tailnetMagicDNSName != "" {
cp.TailnetMagicDNSName = tailnetMagicDNSName
}
pm.knownProfiles[cp.ID] = cp
pm.currentProfile = cp
if err := pm.writeKnownProfiles(); err != nil {
@@ -589,7 +597,7 @@ func (pm *profileManager) migrateFromLegacyPrefs() error {
return fmt.Errorf("load legacy prefs: %w", err)
}
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
if err := pm.SetPrefs(prefs); err != nil {
if err := pm.SetPrefs(prefs, ""); err != nil {
metricMigrationError.Add(1)
return fmt.Errorf("migrating _daemon profile: %w", err)
}

View File

@@ -41,7 +41,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
LoginName: loginName,
},
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -96,7 +96,7 @@ func TestProfileList(t *testing.T) {
LoginName: loginName,
},
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -157,7 +157,7 @@ func TestProfileDupe(t *testing.T) {
reauth := func(pm *profileManager, p *persist.Persist) {
prefs := ipn.NewPrefs()
prefs.Persist = p
must.Do(pm.SetPrefs(prefs.View()))
must.Do(pm.SetPrefs(prefs.View(), ""))
}
login := func(pm *profileManager, p *persist.Persist) {
pm.NewProfile()
@@ -379,7 +379,7 @@ func TestProfileManagement(t *testing.T) {
},
NodeID: nid,
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()
@@ -506,7 +506,7 @@ func TestProfileManagementWindows(t *testing.T) {
},
NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))),
}
if err := pm.SetPrefs(p.View()); err != nil {
if err := pm.SetPrefs(p.View(), ""); err != nil {
t.Fatal(err)
}
return p.View()

View File

@@ -205,7 +205,9 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
return
}
for _, a := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
a := addrs.At(i)
for _, p := range ports {
addrPort := netip.AddrPortFrom(a.Addr(), p)
if _, ok := b.serveListeners[addrPort]; ok {

View File

@@ -378,17 +378,23 @@ func newTestBackend(t *testing.T) *LocalBackend {
},
},
}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{
netip.MustParseAddr("100.150.151.152"): (&tailcfg.Node{
b.peers = map[tailcfg.NodeID]tailcfg.NodeView{
152: (&tailcfg.Node{
ID: 152,
ComputedName: "some-peer",
User: tailcfg.UserID(1),
}).View(),
netip.MustParseAddr("100.150.151.153"): (&tailcfg.Node{
153: (&tailcfg.Node{
ID: 153,
ComputedName: "some-tagged-peer",
Tags: []string{"tag:server", "tag:test"},
User: tailcfg.UserID(1),
}).View(),
}
b.nodeByAddr = map[netip.Addr]tailcfg.NodeID{
netip.MustParseAddr("100.150.151.152"): 152,
netip.MustParseAddr("100.150.151.153"): 153,
}
return b
}

View File

@@ -501,7 +501,7 @@ func TestStateMachine(t *testing.T) {
// (ie. I suspect it would be better to change false->true in send()
// below, and do the same in the real controlclient.)
cc.send(nil, "", false, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -665,7 +665,7 @@ func TestStateMachine(t *testing.T) {
cc.persist.UserProfile.LoginName = "user2"
cc.persist.NodeID = "node2"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(3)
@@ -732,7 +732,7 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nStart4 -> netmap")
notifies.expect(0)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
notifies.drain(0)
@@ -801,7 +801,7 @@ func TestStateMachine(t *testing.T) {
cc.persist.UserProfile.LoginName = "user3"
cc.persist.NodeID = "node3"
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(3)
@@ -845,7 +845,7 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nLoginFinished5")
notifies.expect(2)
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(2)
@@ -862,8 +862,8 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nExpireKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(-time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
Expiry: time.Now().Add(-time.Minute),
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -877,8 +877,8 @@ func TestStateMachine(t *testing.T) {
t.Logf("\n\nExtendKey")
notifies.expect(1)
cc.send(nil, "", false, &netmap.NetworkMap{
Expiry: time.Now().Add(time.Minute),
MachineStatus: tailcfg.MachineAuthorized,
Expiry: time.Now().Add(time.Minute),
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
{
nn := notifies.drain(1)
@@ -923,7 +923,7 @@ func TestEditPrefsHasNoKeys(t *testing.T) {
LegacyFrontendPrivateMachineKey: key.NewMachine(),
},
}).View())
}).View(), "")
if p := b.pm.CurrentPrefs().Persist(); !p.Valid() || p.PrivateNodeKey().IsZero() {
t.Fatalf("PrivateNodeKey not set")
}
@@ -1023,7 +1023,7 @@ func TestWGEngineStatusRace(t *testing.T) {
// Assert that we are logged in and authorized.
cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized,
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
})
wantState(ipn.Starting)

View File

@@ -15,7 +15,6 @@ import (
"slices"
"sort"
"strings"
"sync"
"time"
"tailscale.com/tailcfg"
@@ -257,7 +256,10 @@ type PeerStatus struct {
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
// "funnel"
Capabilities []string `json:",omitempty"`
Capabilities []tailcfg.NodeCapability `json:",omitempty"`
// CapMap is a map of capabilities to their values.
CapMap tailcfg.NodeCapMap `json:",omitempty"`
// SSH_HostKeys are the node's SSH host keys, if known.
SSH_HostKeys []string `json:"sshHostKeys,omitempty"`
@@ -292,13 +294,17 @@ type PeerStatus struct {
Location *tailcfg.Location `json:",omitempty"`
}
// HasCap reports whether ps has the given capability.
func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool {
return ps.CapMap.Contains(cap) || slices.Contains(ps.Capabilities, cap)
}
// StatusBuilder is a request to construct a Status. A new StatusBuilder is
// passed to various subsystems which then call methods on it to populate state.
// Call its Status method to return the final constructed Status.
type StatusBuilder struct {
WantPeers bool // whether caller wants peers
mu sync.Mutex
locked bool
st Status
}
@@ -307,19 +313,13 @@ type StatusBuilder struct {
//
// It may not assume other fields of status are already populated, and
// may not retain or write to the Status after f returns.
//
// MutateStatus acquires a lock so f must not call back into sb.
func (sb *StatusBuilder) MutateStatus(f func(*Status)) {
sb.mu.Lock()
defer sb.mu.Unlock()
f(&sb.st)
}
// Status returns the status that has been built up so far from previous
// calls to MutateStatus, MutateSelfStatus, AddPeer, etc.
func (sb *StatusBuilder) Status() *Status {
sb.mu.Lock()
defer sb.mu.Unlock()
sb.locked = true
return &sb.st
}
@@ -331,8 +331,6 @@ func (sb *StatusBuilder) Status() *Status {
//
// MutateStatus acquires a lock so f must not call back into sb.
func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.st.Self == nil {
sb.st.Self = new(PeerStatus)
}
@@ -341,8 +339,6 @@ func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
// AddUser adds a user profile to the status.
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddUser after Locked")
return
@@ -357,8 +353,6 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
// AddIP adds a Tailscale IP address to the status.
func (sb *StatusBuilder) AddTailscaleIP(ip netip.Addr) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddIP after Locked")
return
@@ -375,8 +369,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
panic("nil PeerStatus")
}
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddPeer after Locked")
return

View File

@@ -557,6 +557,8 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
err = h.b.DebugBreakTCPConns()
case "break-derp-conns":
err = h.b.DebugBreakDERPConns()
case "force-netmap-update":
h.b.DebugForceNetmapUpdate()
case "control-knobs":
k := h.b.ControlKnobs()
w.Header().Set("Content-Type", "application/json")

View File

@@ -755,6 +755,14 @@ type LoginProfile struct {
// It is filled in from the UserProfile.LoginName field.
Name string
// TailnetMagicDNSName is filled with the MagicDNS suffix for this
// profile's node (even if MagicDNS isn't necessarily in use).
// It will neither start nor end with a period.
//
// TailnetMagicDNSName is only filled from 2023-09-09 forward,
// and will only get backfilled when a profile is the current profile.
TailnetMagicDNSName string
// Key is the StateKey under which the profile is stored.
// It is assigned once at profile creation time and never changes.
Key StateKey

View File

@@ -44,6 +44,12 @@ type ServeConfig struct {
// of either the client or the LocalBackend does not expose ports
// that users are not aware of.
Foreground map[string]*ServeConfig `json:",omitempty"`
// ETag is the checksum of the serve config that's populated
// by the LocalClient through the HTTP ETag header during a
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
ETag string `json:"-"`
}
// HostPort is an SNI name and port number, joined by a colon.
@@ -234,7 +240,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for
// Funnel.
func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error {
if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
}
@@ -247,7 +253,7 @@ func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
// CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports.
func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error {
deny := func(allowedPorts string) error {
if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
@@ -256,7 +262,8 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
}
var portsStr string
for _, attr := range nodeAttrs {
if !strings.HasPrefix(attr, tailcfg.CapabilityFunnelPorts) {
attr := string(attr)
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
continue
}
u, err := url.Parse(attr)
@@ -268,7 +275,7 @@ func CheckFunnelPort(wantedPort uint16, nodeAttrs []string) error {
return deny("")
}
u.RawQuery = ""
if u.String() != tailcfg.CapabilityFunnelPorts {
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
return deny("")
}
}

View File

@@ -9,20 +9,21 @@ import (
)
func TestCheckFunnelAccess(t *testing.T) {
portAttr := "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c }
const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443,"
tests := []struct {
port uint16
caps []string
caps []tailcfg.NodeCapability
wantErr bool
}{
{443, []string{portAttr}, true}, // No "funnel" attribute
{443, []string{portAttr, tailcfg.NodeAttrFunnel}, true},
{443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8443, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8321, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{8083, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, false},
{8091, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{3000, []string{portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel}, true},
{443, caps(portAttr), true}, // No "funnel" attribute
{443, caps(portAttr, tailcfg.NodeAttrFunnel), true},
{443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false},
{8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
}
for _, tt := range tests {
err := CheckFunnelAccess(tt.port, tt.caps)

View File

@@ -74,6 +74,7 @@ Some packages may only be included on certain architectures or operating systems
- [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/f0b76a10a08e/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/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/68bd39ee4109/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/93bd5cbf7fd8/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))

View File

@@ -38,15 +38,15 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [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/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/a3cf94ed774a/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/84569fd814a9/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.0/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/ad4cb58a6516/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.13.0:LICENSE))
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/515e97eb:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.12.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/9a58c47922fd/LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))

View File

@@ -97,7 +97,9 @@ func (m *Manager) Set(cfg Config) error {
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)
}))
m.logf("OScfg: %+v", ocfg)
m.logf("OScfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
ocfg.WriteToBufioWriter(w)
}))
if err := m.resolver.SetConfig(rcfg); err != nil {
return err

View File

@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/netip"
"strings"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
@@ -65,6 +66,42 @@ type OSConfig struct {
MatchDomains []dnsname.FQDN
}
func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) {
if o == nil {
w.WriteString("<nil>")
return
}
w.WriteString("{")
if len(o.Hosts) > 0 {
fmt.Fprintf(w, "Hosts:%v ", o.Hosts)
}
if len(o.Nameservers) > 0 {
fmt.Fprintf(w, "Nameservers:%v ", o.Nameservers)
}
if len(o.SearchDomains) > 0 {
fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains)
}
if len(o.MatchDomains) > 0 {
w.WriteString("SearchDomains:[")
sp := ""
var numARPA int
for _, s := range o.MatchDomains {
if strings.HasSuffix(string(s), ".arpa.") {
numARPA++
continue
}
w.WriteString(sp)
w.WriteString(string(s))
sp = " "
}
w.WriteString("]")
if numARPA > 0 {
fmt.Fprintf(w, "+%darpa", numARPA)
}
}
w.WriteString("}")
}
func (o OSConfig) IsZero() bool {
return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0
}

View File

@@ -137,6 +137,7 @@ const (
// populate is called once to initialize the knownDoH and dohIPsOfBase maps.
func populate() {
// Cloudflare
// https://developers.cloudflare.com/1.1.1.1/ip-addresses/
addDoH("1.1.1.1", "https://cloudflare-dns.com/dns-query")
addDoH("1.0.0.1", "https://cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1111", "https://cloudflare-dns.com/dns-query")
@@ -170,10 +171,17 @@ func populate() {
// addDoH("208.67.220.123", "https://doh.familyshield.opendns.com/dns-query")
// Quad9
// https://www.quad9.net/service/service-addresses-and-features
addDoH("9.9.9.9", "https://dns.quad9.net/dns-query")
addDoH("149.112.112.112", "https://dns.quad9.net/dns-query")
addDoH("2620:fe::fe", "https://dns.quad9.net/dns-query")
addDoH("2620:fe::fe:9", "https://dns.quad9.net/dns-query")
addDoH("2620:fe::9", "https://dns.quad9.net/dns-query")
// Quad9 +ECS +DNSSEC
addDoH("9.9.9.11", "https://dns11.quad9.net/dns-query")
addDoH("149.112.112.11", "https://dns11.quad9.net/dns-query")
addDoH("2620:fe::11", "https://dns11.quad9.net/dns-query")
addDoH("2620:fe::fe:11", "https://dns11.quad9.net/dns-query")
// Quad9 -DNSSEC
addDoH("9.9.9.10", "https://dns10.quad9.net/dns-query")
@@ -182,14 +190,22 @@ func populate() {
addDoH("2620:fe::fe:10", "https://dns10.quad9.net/dns-query")
// Mullvad
addDoH("194.242.2.2", "https://doh.mullvad.net/dns-query")
addDoH("193.19.108.2", "https://doh.mullvad.net/dns-query")
addDoH("2a07:e340::2", "https://doh.mullvad.net/dns-query")
// Mullvad -Ads
addDoH("194.242.2.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("193.19.108.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.doh.mullvad.net/dns-query")
// See https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
// Mullvad (default)
addDoH("194.242.2.2", "https://dns.mullvad.net/dns-query")
addDoH("2a07:e340::2", "https://dns.mullvad.net/dns-query")
// Mullvad (adblock)
addDoH("194.242.2.3", "https://adblock.dns.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.dns.mullvad.net/dns-query")
// Mullvad (base)
addDoH("194.242.2.4", "https://base.dns.mullvad.net/dns-query")
addDoH("2a07:e340::4", "https://base.dns.mullvad.net/dns-query")
// Mullvad (extended)
addDoH("194.242.2.5", "https://extended.dns.mullvad.net/dns-query")
addDoH("2a07:e340::5", "https://extended.dns.mullvad.net/dns-query")
// Mullvad (all)
addDoH("194.242.2.9", "https://all.dns.mullvad.net/dns-query")
addDoH("2a07:e340::9", "https://all.dns.mullvad.net/dns-query")
// Wikimedia
addDoH(wikimediaDNSv4, "https://wikimedia-dns.org/dns-query")

View File

@@ -303,7 +303,7 @@ func (p *Pinger) Send(ctx context.Context, dest net.Addr, data []byte) (time.Dur
m := icmp.Message{
Type: icmpType,
Code: icmpType.Protocol(),
Code: 0,
Body: &icmp.Echo{
ID: int(p.id),
Seq: int(seq),

View File

@@ -28,6 +28,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
)
// DebugKnobs contains debug configuration that can be provided when creating a
@@ -1024,3 +1025,22 @@ var (
// we received a UPnP response with a new meta.
metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
)
// UPnP error metric that's keyed by code; lazily registered on first read
var (
metricUPnPErrorsByCodeMu sync.Mutex
metricUPnPErrorsByCode map[int]*clientmetric.Metric
)
func getUPnPErrorsMetric(code int) *clientmetric.Metric {
metricUPnPErrorsByCodeMu.Lock()
defer metricUPnPErrorsByCodeMu.Unlock()
mm := metricUPnPErrorsByCode[code]
if mm != nil {
return mm
}
mm = clientmetric.NewCounter(fmt.Sprintf("portmap_upnp_errors_with_code_%d", code))
mak.Set(&metricUPnPErrorsByCode, code, mm)
return mm
}

View File

@@ -337,9 +337,14 @@ func (c *Client) getUPnPPortMapping(
// duration; see the following issue for details:
// https://github.com/tailscale/tailscale/issues/9343
if err != nil {
code, ok := getUPnPErrorCode(err)
if ok {
getUPnPErrorsMetric(code).Add(1)
}
// From the UPnP spec: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// 725: OnlyPermanentLeasesSupported
if isUPnPError(err, 725) {
if ok && code == 725 {
newPort, err = addAnyPortMapping(
ctx,
client,
@@ -387,13 +392,13 @@ func (c *Client) getUPnPPortMapping(
return upnp.external, true
}
// isUPnPError returns whether the provided error is a UPnP error response with
// the given error code. It returns false if the error is not a SOAP error, or
// the inner error details are not a UPnP error.
func isUPnPError(err error, errCode int) bool {
// getUPnPErrorCode returns the UPnP error code from the given response, if the
// error is a SOAP error in the proper format, and a boolean indicating whether
// the provided error was actually a UPnP error.
func getUPnPErrorCode(err error) (int, bool) {
soapErr, ok := err.(*soap.SOAPFaultError)
if !ok {
return false
return 0, false
}
var upnpErr struct {
@@ -402,13 +407,12 @@ func isUPnPError(err error, errCode int) bool {
Description string `xml:"errorDescription"`
}
if err := xml.Unmarshal([]byte(soapErr.Detail.Raw), &upnpErr); err != nil {
return false
return 0, false
}
if upnpErr.XMLName.Local != "UPnPError" {
return false
return 0, false
}
return upnpErr.Code == errCode
return upnpErr.Code, true
}
type uPnPDiscoResponse struct {

View File

@@ -161,6 +161,11 @@ type oncePrefix struct {
v netip.Prefix
}
// FalseContainsIPFunc is shorthand for NewContainsIPFunc(views.Slice[netip.Prefix]{}).
func FalseContainsIPFunc() func(ip netip.Addr) bool {
return func(ip netip.Addr) bool { return false }
}
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
//
// It's optimized for the cases of addrs being empty and addrs
@@ -168,20 +173,17 @@ type oncePrefix struct {
// one IPv6 address).
//
// Otherwise the implementation is somewhat slow.
func NewContainsIPFunc(addrs []netip.Prefix) func(ip netip.Addr) bool {
func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool {
// Specialize the three common cases: no address, just IPv4
// (or just IPv6), and both IPv4 and IPv6.
if len(addrs) == 0 {
if addrs.Len() == 0 {
return func(netip.Addr) bool { return false }
}
// If any addr is more than a single IP, then just do the slow
// linear thing until
// https://github.com/inetaf/netaddr/issues/139 is done.
for _, a := range addrs {
if a.IsSingleIP() {
continue
}
acopy := append([]netip.Prefix(nil), addrs...)
if views.SliceContainsFunc(addrs, func(p netip.Prefix) bool { return !p.IsSingleIP() }) {
acopy := addrs.AsSlice()
return func(ip netip.Addr) bool {
for _, a := range acopy {
if a.Contains(ip) {
@@ -192,18 +194,18 @@ func NewContainsIPFunc(addrs []netip.Prefix) func(ip netip.Addr) bool {
}
}
// Fast paths for 1 and 2 IPs:
if len(addrs) == 1 {
a := addrs[0]
if addrs.Len() == 1 {
a := addrs.At(0)
return func(ip netip.Addr) bool { return ip == a.Addr() }
}
if len(addrs) == 2 {
a, b := addrs[0], addrs[1]
if addrs.Len() == 2 {
a, b := addrs.At(0), addrs.At(1)
return func(ip netip.Addr) bool { return ip == a.Addr() || ip == b.Addr() }
}
// General case:
m := map[netip.Addr]bool{}
for _, a := range addrs {
m[a.Addr()] = true
for i := range addrs.LenIter() {
m[addrs.At(i).Addr()] = true
}
return func(ip netip.Addr) bool { return m[ip] }
}

View File

@@ -8,6 +8,7 @@ import (
"testing"
"tailscale.com/net/netaddr"
"tailscale.com/types/views"
)
func TestInCrostiniRange(t *testing.T) {
@@ -66,29 +67,29 @@ func TestCGNATRange(t *testing.T) {
}
func TestNewContainsIPFunc(t *testing.T) {
f := NewContainsIPFunc([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")})
f := NewContainsIPFunc(views.SliceOf([]netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}))
if f(netip.MustParseAddr("8.8.8.8")) {
t.Fatal("bad")
}
if !f(netip.MustParseAddr("10.1.2.3")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{netip.MustParsePrefix("10.1.2.3/32")})
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{netip.MustParsePrefix("10.1.2.3/32")}))
if !f(netip.MustParseAddr("10.1.2.3")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{
netip.MustParsePrefix("10.1.2.3/32"),
netip.MustParsePrefix("::2/128"),
})
}))
if !f(netip.MustParseAddr("::2")) {
t.Fatal("bad")
}
f = NewContainsIPFunc([]netip.Prefix{
f = NewContainsIPFunc(views.SliceOf([]netip.Prefix{
netip.MustParsePrefix("10.1.2.3/32"),
netip.MustParsePrefix("10.1.2.4/32"),
netip.MustParsePrefix("::2/128"),
})
}))
if !f(netip.MustParseAddr("10.1.2.4")) {
t.Fatal("bad")
}

View File

@@ -35,14 +35,15 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
ret := make(dnsMap)
suffix := nm.MagicDNSSuffix()
have4 := false
if nm.Name != "" && len(nm.Addresses) > 0 {
ip := nm.Addresses[0].Addr()
addrs := nm.GetAddresses()
if nm.Name != "" && addrs.Len() > 0 {
ip := addrs.At(0).Addr()
ret[canonMapKey(nm.Name)] = ip
if dnsname.HasSuffix(nm.Name, suffix) {
ret[canonMapKey(dnsname.TrimSuffix(nm.Name, suffix))] = ip
}
for _, a := range nm.Addresses {
if a.Addr().Is4() {
for i := range addrs.LenIter() {
if addrs.At(i).Addr().Is4() {
have4 = true
}
}

View File

@@ -32,10 +32,12 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
}).View(),
},
want: dnsMap{
"foo": ip("100.102.103.104"),
@@ -46,10 +48,12 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self_and_peers",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100.102.103.104/32"),
pfx("100::123/128"),
},
}).View(),
Peers: []tailcfg.NodeView{
(&tailcfg.Node{
Name: "a.tailnet",
@@ -79,9 +83,11 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
name: "self_has_v6_only",
nm: &netmap.NetworkMap{
Name: "foo.tailnet",
Addresses: []netip.Prefix{
pfx("100::123/128"),
},
SelfNode: (&tailcfg.Node{
Addresses: []netip.Prefix{
pfx("100::123/128"),
},
}).View(),
Peers: nodeViews([]*tailcfg.Node{
{
Name: "a.tailnet",

View File

@@ -5,8 +5,8 @@ package tstun
import "tailscale.com/envknob"
const (
maxMTU uint32 = 65536
defaultMTU uint32 = 1280
maxMTU = 65536
defaultMTU = 1280
)
// DefaultMTU returns either the constant default MTU of 1280, or the value set
@@ -21,13 +21,8 @@ func DefaultMTU() uint32 {
// 1280 is the smallest MTU allowed for IPv6, which is a sensible
// "probably works everywhere" setting until we develop proper PMTU
// discovery.
tunMTU := defaultMTU
if mtu, ok := envknob.LookupUintSized("TS_DEBUG_MTU", 10, 32); ok {
mtu := uint32(mtu)
if mtu > maxMTU {
mtu = maxMTU
}
tunMTU = mtu
return min(uint32(mtu), maxMTU)
}
return tunMTU
return defaultMTU
}

View File

@@ -98,7 +98,7 @@ type Wrapper struct {
// timeNow, if non-nil, will be used to obtain the current time.
timeNow func() time.Time
// natV4Config stores the current NAT configuration.
// natV4Config stores the current IPv4 NAT configuration.
natV4Config atomic.Pointer[natV4Config]
// vectorBuffer stores the oldest unconsumed packet vector from tdev. It is
@@ -545,6 +545,44 @@ type natV4Config struct {
dstAddrToPeerKeyMapper *table.RoutingTable
}
func (c *natV4Config) String() string {
if c == nil {
return "<nil>"
}
var b strings.Builder
b.WriteString("natV4Config{")
fmt.Fprintf(&b, "nativeAddr: %v, ", c.nativeAddr)
fmt.Fprint(&b, "listenAddrs: [")
i := 0
c.listenAddrs.Range(func(k netip.Addr, _ struct{}) bool {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(k.String())
i++
return true
})
count := map[netip.Addr]int{}
c.dstMasqAddrs.Range(func(_ key.NodePublic, v netip.Addr) bool {
count[v]++
return true
})
i = 0
b.WriteString("], dstMasqAddrs: [")
for k, v := range count {
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(&b, "%v: %v peers", k, v)
i++
}
b.WriteString("]}")
return b.String()
}
// mapDstIP returns the destination IP to use for a packet to dst.
// If dst is not one of the listen addresses, it is returned as-is,
// otherwise the native address is returned.
@@ -577,9 +615,9 @@ func (c *natV4Config) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
return oldSrc
}
// natConfigFromWireGuardConfig generates a natV4Config from nm.
// natV4ConfigFromWGConfig generates a natV4Config from nm.
// If v4 NAT is not required, it returns nil.
func natConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
func natV4ConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
if wcfg == nil {
return nil
}
@@ -632,10 +670,10 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config) *natV4Config {
// SetNetMap is called when a new NetworkMap is received.
// It currently (2023-03-01) only updates the IPv4 NAT configuration.
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
cfg := natConfigFromWGConfig(wcfg)
cfg := natV4ConfigFromWGConfig(wcfg)
old := t.natV4Config.Swap(cfg)
if !reflect.DeepEqual(old, cfg) {
t.logf("nat config: %+v", cfg)
t.logf("nat config: %v", cfg)
}
}

View File

@@ -780,7 +780,7 @@ func TestNATCfg(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ncfg := natConfigFromWGConfig(tc.wcfg)
ncfg := natV4ConfigFromWGConfig(tc.wcfg)
for peer, want := range tc.snatMap {
if got := ncfg.selectSrcIP(selfNativeIP, peer); got != want {
t.Errorf("selectSrcIP[%v]: got %v; want %v", peer, got, want)

72
proxymap/proxymap.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package proxymap contains a mapping table for ephemeral localhost ports used
// by tailscaled on behalf of remote Tailscale IPs for proxied connections.
package proxymap
import (
"net/netip"
"sync"
"time"
"tailscale.com/util/mak"
)
// Mapper tracks which localhost ip:ports correspond to which remote Tailscale
// IPs for connections proxied by tailscaled.
//
// This is then used (via the WhoIsIPPort method) by localhost applications to
// ask tailscaled (via the LocalAPI WhoIs method) the Tailscale identity that a
// given localhost:port corresponds to.
type Mapper struct {
mu sync.Mutex
m map[netip.AddrPort]netip.Addr
}
// RegisterIPPortIdentity registers a given node (identified by its
// Tailscale IP) as temporarily having the given IP:port for whois lookups.
// The IP:port is generally a localhost IP and an ephemeral port, used
// while proxying connections to localhost when tailscaled is running
// in netstack mode.
func (m *Mapper) RegisterIPPortIdentity(ipport netip.AddrPort, tsIP netip.Addr) {
m.mu.Lock()
defer m.mu.Unlock()
mak.Set(&m.m, ipport, tsIP)
}
// UnregisterIPPortIdentity removes a temporary IP:port registration
// made previously by RegisterIPPortIdentity.
func (m *Mapper) UnregisterIPPortIdentity(ipport netip.AddrPort) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.m, ipport)
}
var whoIsSleeps = [...]time.Duration{
0,
10 * time.Millisecond,
20 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
}
// WhoIsIPPort looks up an IP:port in the temporary registrations,
// and returns a matching Tailscale IP, if it exists.
func (m *Mapper) WhoIsIPPort(ipport netip.AddrPort) (tsIP netip.Addr, ok bool) {
// We currently have a registration race,
// https://github.com/tailscale/tailscale/issues/1616,
// so loop a few times for now waiting for the registration
// to appear.
// TODO(bradfitz,namansood): remove this once #1616 is fixed.
for _, d := range whoIsSleeps {
time.Sleep(d)
m.mu.Lock()
tsIP, ok = m.m[ipport]
m.mu.Unlock()
if ok {
return tsIP, true
}
}
return tsIP, false
}

View File

@@ -269,7 +269,12 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
}
sort.Strings(envStrs)
buildDir := b.TmpDir()
args := []string{"build", "-v", "-o", buildDir}
outPath := buildDir
if env["GOOS"] == "windowsdll" {
// DLL builds fail unless we use a fully-qualified path to the output binary.
outPath = filepath.Join(buildDir, filepath.Base(path)+".dll")
}
args := []string{"build", "-v", "-o", outPath}
if len(tags) > 0 {
tagsStr := strings.Join(tags, ",")
log.Printf("Building %s (with env %s, tags %s)", path, strings.Join(envStrs, " "), tagsStr)
@@ -288,6 +293,8 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
out := filepath.Join(buildDir, filepath.Base(path))
if env["GOOS"] == "windows" || env["GOOS"] == "windowsgui" {
out += ".exe"
} else if env["GOOS"] == "windowsdll" {
out += ".dll"
}
return out, nil
})

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-TZP/FQqb21yiKMlIPXXSoN6HfiBAun+gPZHQ5cPc8L0=
# nix-direnv cache busting line: sha256-aVtlDzC+sbEWlUAzPkAryA/+dqSzoAFc02xikh6yhf8=

View File

@@ -12,9 +12,11 @@ import (
"fmt"
"net/netip"
"reflect"
"slices"
"strings"
"time"
"golang.org/x/exp/maps"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
@@ -111,7 +113,8 @@ type CapabilityVersion int
// - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
// - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
// - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
const CurrentCapabilityVersion CapabilityVersion = 73
// - 74: 2023-09-18: Client understands NodeCapMap
const CurrentCapabilityVersion CapabilityVersion = 74
type StableID string
@@ -215,6 +218,31 @@ func (emptyStructJSONSlice) MarshalJSON() ([]byte, error) {
func (emptyStructJSONSlice) UnmarshalJSON([]byte) error { return nil }
// RawMessage is a raw encoded JSON value. It implements Marshaler and
// Unmarshaler and can be used to delay JSON decoding or precompute a JSON
// encoding.
//
// It is like json.RawMessage but is a string instead of a []byte to better
// portray immutable data.
type RawMessage string
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == "" {
return []byte("null"), nil
}
return []byte(m), nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("RawMessage: UnmarshalJSON on nil pointer")
}
*m = RawMessage(data)
return nil
}
type Node struct {
ID NodeID
StableID StableNodeID
@@ -289,7 +317,21 @@ type Node struct {
// such as:
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
Capabilities []string `json:",omitempty"`
//
// Deprecated: use CapMap instead.
Capabilities []NodeCapability `json:",omitempty"`
// CapMap is a map of capabilities to their optional argument/data values.
//
// It is valid for a capability to not have any argument/data values; such
// capabilities can be tested for using the HasCap method. These type of
// capabilities are used to indicate that a node has a capability, but there
// is no additional data associated with it. These were previously
// represented by the Capabilities field, but can now be represented by
// CapMap with an empty value.
//
// See NodeCapability for more information on keys.
CapMap NodeCapMap `json:",omitempty"`
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
@@ -332,11 +374,41 @@ type Node struct {
// not be masqueraded (e.g. in case of --snat-subnet-routes).
SelfNodeV4MasqAddrForThisPeer *netip.Addr `json:",omitempty"`
// SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as.
// It may be empty if the peer knows the current node by its native
// IPv6 address.
// This field is only populated in a MapResponse for peers and not
// for the current node.
//
// If set, it should be used to masquerade traffic originating from the
// current node to this peer. The masquerade address is only relevant
// for this peer and not for other peers.
//
// This only applies to traffic originating from the current node to the
// peer or any of its subnets. Traffic originating from subnet routes will
// not be masqueraded (e.g. in case of --snat-subnet-routes).
SelfNodeV6MasqAddrForThisPeer *netip.Addr `json:",omitempty"`
// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it
// is not expected to speak Disco or DERP, and it must have Endpoints in
// order to be reachable. TODO(#7826): 2023-04-06: only the first parseable
// Endpoint is used, see #7826 for updates.
// order to be reachable.
IsWireGuardOnly bool `json:",omitempty"`
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
// node is marked IsWireGuardOnly and being used as an exit node.
ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
}
// HasCap reports whether the node has the given capability.
// It is safe to call on an invalid NodeView.
func (v NodeView) HasCap(cap NodeCapability) bool {
return v.ж.HasCap(cap)
}
// HasCap reports whether the node has the given capability.
// It is safe to call on a nil Node.
func (v *Node) HasCap(cap NodeCapability) bool {
return v != nil && (v.CapMap.Contains(cap) || slices.Contains(v.Capabilities, cap))
}
// DisplayName returns the user-facing name for a node which should
@@ -1223,10 +1295,11 @@ type CapGrant struct {
CapMap PeerCapMap `json:",omitempty"`
}
// PeerCapability is a capability granted to a node by a FilterRule.
// It's a string, but its meaning is application-defined.
// It must be a URL, like "https://tailscale.com/cap/file-sharing-target" or
// "https://example.com/cap/read-access".
// PeerCapability represents a capability granted to a peer by a FilterRule when
// the peer communicates with the node that has this rule. Its meaning is
// application-defined.
//
// It must be a URL like "https://tailscale.com/cap/file-send".
type PeerCapability string
const (
@@ -1245,13 +1318,52 @@ const (
PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress"
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the Contains method.
//
// See [NodeCapability] for more information on keys.
type NodeCapMap map[NodeCapability][]RawMessage
// Equal reports whether c and c2 are equal.
func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
return maps.EqualFunc(c, c2, slices.Equal)
}
// UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) {
vals, ok := cm[cap]
if !ok {
return nil, nil
}
out := make([]T, 0, len(vals))
for _, v := range vals {
var t T
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
// Contains reports whether c has the capability cap. This is used to test for
// the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c NodeCapMap) Contains(cap NodeCapability) bool {
_, ok := c[cap]
return ok
}
// PeerCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the HasCapability method.
//
// The values are opaque to Tailscale, but are passed through from the ACLs to
// the application via the WhoIs API.
type PeerCapMap map[PeerCapability][]json.RawMessage
type PeerCapMap map[PeerCapability][]RawMessage
// UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
@@ -1264,7 +1376,7 @@ func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
out := make([]T, 0, len(vals))
for _, v := range vals {
var t T
if err := json.Unmarshal(v, &t); err != nil {
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
@@ -1272,9 +1384,9 @@ func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) {
return out, nil
}
// HasCapability reports whether c has the capability cap.
// This is used to test for the existence of a capability, especially
// when the capability has no values.
// HasCapability reports whether c has the capability cap. This is used to test
// for the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c PeerCapMap) HasCapability(cap PeerCapability) bool {
_, ok := c[cap]
return ok
@@ -1835,13 +1947,15 @@ func (n *Node) Equal(n2 *Node) bool {
n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) &&
n.MachineAuthorized == n2.MachineAuthorized &&
eqStrings(n.Capabilities, n2.Capabilities) &&
slices.Equal(n.Capabilities, n2.Capabilities) &&
n.CapMap.Equal(n2.CapMap) &&
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
eqStrings(n.Tags, n2.Tags) &&
n.Expired == n2.Expired &&
eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) &&
eqPtr(n.SelfNodeV6MasqAddrForThisPeer, n2.SelfNodeV6MasqAddrForThisPeer) &&
n.IsWireGuardOnly == n2.IsWireGuardOnly
}
@@ -1908,112 +2022,121 @@ type Oauth2Token struct {
Expiry time.Time `json:"expiry,omitempty"`
}
const (
// These are the capabilities that the self node has as listed in
// MapResponse.Node.Capabilities.
//
// We've since started referring to these as "Node Attributes" ("nodeAttrs"
// in the ACL policy file).
// NodeCapability represents a capability granted to the self node as listed in
// MapResponse.Node.Capabilities.
//
// It must be a URL like "https://tailscale.com/cap/file-sharing", or a
// well-known capability name like "funnel". The latter is only allowed for
// Tailscale-defined capabilities.
//
// Unlike PeerCapability, NodeCapability is not in context of a peer and is
// granted to the node itself.
//
// These are also referred to as "Node Attributes" in the ACL policy file.
type NodeCapability string
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
CapabilityHTTPS = "https" // https cert provisioning enabled on tailnet
const (
CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin"
CapabilitySSH NodeCapability = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilitySSHRuleIn NodeCapability = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug NodeCapability = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
CapabilityHTTPS NodeCapability = "https" // https cert provisioning enabled on tailnet
// CapabilityBindToInterfaceByRoute changes how Darwin nodes create
// sockets (in the net/netns package). See that package for more
// details on the behaviour of this capability.
CapabilityBindToInterfaceByRoute = "https://tailscale.com/cap/bind-to-interface-by-route"
CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route"
// CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin
// nodes get the default interface. There is an optional hook (used by the
// macOS and iOS clients) to override the default interface, this capability
// disables that and uses the default behavior (of parsing the routing
// table).
CapabilityDebugDisableAlternateDefaultRouteInterface = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface"
CapabilityDebugDisableAlternateDefaultRouteInterface NodeCapability = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface"
// CapabilityDebugDisableBindConnToInterface disables the automatic binding
// of connections to the default network interface on Darwin nodes.
CapabilityDebugDisableBindConnToInterface = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"
CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"
// CapabilityTailnetLock indicates the node may initialize tailnet lock.
CapabilityTailnetLock = "https://tailscale.com/cap/tailnet-lock"
CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock"
// Funnel warning capabilities used for reporting errors to the user.
// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
CapabilityWarnFunnelNoInvite = "https://tailscale.com/cap/warn-funnel-no-invite"
CapabilityWarnFunnelNoInvite NodeCapability = "https://tailscale.com/cap/warn-funnel-no-invite"
// CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
CapabilityWarnFunnelNoHTTPS = "https://tailscale.com/cap/warn-funnel-no-https"
CapabilityWarnFunnelNoHTTPS NodeCapability = "https://tailscale.com/cap/warn-funnel-no-https"
// Debug logging capabilities
// CapabilityDebugTSDNSResolution enables verbose debug logging for DNS
// resolution for Tailscale-controlled domains (the control server, log
// server, DERP servers, etc.)
CapabilityDebugTSDNSResolution = "https://tailscale.com/cap/debug-ts-dns-resolution"
CapabilityDebugTSDNSResolution NodeCapability = "https://tailscale.com/cap/debug-ts-dns-resolution"
// CapabilityFunnelPorts specifies the ports that the Funnel is available on.
// The ports are specified as a comma-separated list of port numbers or port
// ranges (e.g. "80,443,8080-8090") in the ports query parameter.
// e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090
CapabilityFunnelPorts = "https://tailscale.com/cap/funnel-ports"
)
CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports"
const (
// NodeAttrFunnel grants the ability for a node to host ingress traffic.
NodeAttrFunnel = "funnel"
NodeAttrFunnel NodeCapability = "funnel"
// NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions.
NodeAttrSSHAggregator = "ssh-aggregator"
NodeAttrSSHAggregator NodeCapability = "ssh-aggregator"
// NodeAttrDebugForceBackgroundSTUN forces a node to always do background
// STUN queries regardless of inactivity.
NodeAttrDebugForceBackgroundSTUN = "debug-always-stun"
NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun"
// NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration,
// always giving WireGuard the full netmap, even for idle peers.
NodeAttrDebugDisableWGTrim = "debug-no-wg-trim"
NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim"
// NodeAttrDebugDisableDRPO disables the DERP Return Path Optimization.
// See Issue 150.
NodeAttrDebugDisableDRPO = "debug-disable-drpo"
NodeAttrDebugDisableDRPO NodeCapability = "debug-disable-drpo"
// NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network.
NodeAttrDisableSubnetsIfPAC = "debug-disable-subnets-if-pac"
NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac"
// NodeAttrDisableUPnP makes the client not perform a UPnP portmapping.
// By default, we want to enable it to see if it works on more clients.
//
// If UPnP catastrophically fails for people, this should be set kill
// new attempts at UPnP connections.
NodeAttrDisableUPnP = "debug-disable-upnp"
NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp"
// NodeAttrDisableDeltaUpdates makes the client not process updates via the
// delta update mechanism and should instead treat all netmap changes as
// "full" ones as tailscaled did in 1.48.x and earlier.
NodeAttrDisableDeltaUpdates = "disable-delta-updates"
NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates"
// NodeAttrRandomizeClientPort makes magicsock UDP bind to
// :0 to get a random local port, ignoring any configured
// fixed port.
NodeAttrRandomizeClientPort = "randomize-client-port"
NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port"
// NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route
// rather than a /32 per peer. At most one of this or
// NodeAttrOneCGNATDisable may be set; if neither are, it's automatic.
NodeAttrOneCGNATEnable = "one-cgnat?v=true"
NodeAttrOneCGNATEnable NodeCapability = "one-cgnat?v=true"
// NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer
// rather than one big /10 CGNAT route. At most one of this or
// NodeAttrOneCGNATEnable may be set; if neither are, it's automatic.
NodeAttrOneCGNATDisable = "one-cgnat?v=false"
NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false"
// NodeAttrPeerMTUEnable makes the client do path MTU discovery to its
// peers. If it isn't set, it defaults to the client default.
NodeAttrPeerMTUEnable NodeCapability = "peer-mtu-enable"
)
// SetDNSRequest is a request to add a DNS record.
@@ -2405,6 +2528,9 @@ type PeerChange struct {
// Cap, if non-zero, means that NodeID's capability version has changed.
Cap CapabilityVersion `json:",omitempty"`
// CapMap, if non-nil, means that NodeID's capability map has changed.
CapMap NodeCapMap `json:",omitempty"`
// Endpoints, if non-empty, means that NodeID's UDP Endpoints
// have changed to these.
Endpoints []string `json:",omitempty"`
@@ -2431,7 +2557,7 @@ type PeerChange struct {
// Capabilities, if non-nil, means that the NodeID's capabilities changed.
// It's a pointer to a slice for "omitempty", to allow differentiating
// a change to empty from no change.
Capabilities *[]string `json:",omitempty"`
Capabilities *[]NodeCapability `json:",omitempty"`
}
// DerpMagicIP is a fake WireGuard endpoint IP address that means to

View File

@@ -62,9 +62,24 @@ func (src *Node) Clone() *Node {
dst.Online = ptr.To(*src.Online)
}
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
if dst.CapMap != nil {
dst.CapMap = map[NodeCapability][]RawMessage{}
for k := range src.CapMap {
dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...)
}
}
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
}
if dst.SelfNodeV6MasqAddrForThisPeer != nil {
dst.SelfNodeV6MasqAddrForThisPeer = ptr.To(*src.SelfNodeV6MasqAddrForThisPeer)
}
if src.ExitNodeDNSResolvers != nil {
dst.ExitNodeDNSResolvers = make([]*dnstype.Resolver, len(src.ExitNodeDNSResolvers))
for i := range dst.ExitNodeDNSResolvers {
dst.ExitNodeDNSResolvers[i] = src.ExitNodeDNSResolvers[i].Clone()
}
}
return dst
}
@@ -92,7 +107,8 @@ var _NodeCloneNeedsRegeneration = Node(struct {
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []string
Capabilities []NodeCapability
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -100,7 +116,9 @@ var _NodeCloneNeedsRegeneration = Node(struct {
DataPlaneAuditLogID string
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
SelfNodeV6MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// Clone makes a deep copy of Hostinfo.
@@ -219,9 +237,11 @@ func (src *DNSConfig) Clone() *DNSConfig {
}
dst := new(DNSConfig)
*dst = *src
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers {
dst.Resolvers[i] = src.Resolvers[i].Clone()
if src.Resolvers != nil {
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers {
dst.Resolvers[i] = src.Resolvers[i].Clone()
}
}
if dst.Routes != nil {
dst.Routes = map[string][]*dnstype.Resolver{}
@@ -229,9 +249,11 @@ func (src *DNSConfig) Clone() *DNSConfig {
dst.Routes[k] = append([]*dnstype.Resolver{}, src.Routes[k]...)
}
}
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
for i := range dst.FallbackResolvers {
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
if src.FallbackResolvers != nil {
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
for i := range dst.FallbackResolvers {
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
}
}
dst.Domains = append(src.Domains[:0:0], src.Domains...)
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
@@ -365,9 +387,11 @@ func (src *DERPRegion) Clone() *DERPRegion {
}
dst := new(DERPRegion)
*dst = *src
dst.Nodes = make([]*DERPNode, len(src.Nodes))
for i := range dst.Nodes {
dst.Nodes[i] = src.Nodes[i].Clone()
if src.Nodes != nil {
dst.Nodes = make([]*DERPNode, len(src.Nodes))
for i := range dst.Nodes {
dst.Nodes[i] = src.Nodes[i].Clone()
}
}
return dst
}
@@ -444,9 +468,11 @@ func (src *SSHRule) Clone() *SSHRule {
if dst.RuleExpires != nil {
dst.RuleExpires = ptr.To(*src.RuleExpires)
}
dst.Principals = make([]*SSHPrincipal, len(src.Principals))
for i := range dst.Principals {
dst.Principals[i] = src.Principals[i].Clone()
if src.Principals != nil {
dst.Principals = make([]*SSHPrincipal, len(src.Principals))
for i := range dst.Principals {
dst.Principals[i] = src.Principals[i].Clone()
}
}
dst.SSHUsers = maps.Clone(src.SSHUsers)
dst.Action = src.Action.Clone()

View File

@@ -346,11 +346,11 @@ func TestNodeEqual(t *testing.T) {
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "Cap", "Tags", "PrimaryRoutes",
"LastSeen", "Online", "MachineAuthorized",
"Capabilities",
"Capabilities", "CapMap",
"UnsignedPeerAPIOnly",
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
"DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer",
"IsWireGuardOnly",
"SelfNodeV6MasqAddrForThisPeer", "IsWireGuardOnly", "ExitNodeDNSResolvers",
}
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
@@ -545,6 +545,55 @@ func TestNodeEqual(t *testing.T) {
&Node{SelfNodeV4MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("100.64.0.1"))},
true,
},
{
&Node{},
&Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))},
false,
},
{
&Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))},
&Node{SelfNodeV6MasqAddrForThisPeer: ptr.To(netip.MustParseAddr("2001::3456"))},
true,
},
{
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"foo"`},
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"foo"`},
},
},
true,
},
{
&Node{
CapMap: NodeCapMap{
"bar": []RawMessage{`"foo"`},
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"bar"`},
},
},
false,
},
{
&Node{
CapMap: NodeCapMap{
"foo": nil,
},
},
&Node{
CapMap: NodeCapMap{
"foo": []RawMessage{`"bar"`},
},
},
false,
},
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)
@@ -726,3 +775,82 @@ func TestUnmarshalHealth(t *testing.T) {
}
}
}
func TestRawMessage(t *testing.T) {
// Create a few types of json.RawMessages and then marshal them back and
// forth to make sure they round-trip.
type rule struct {
Ports []int `json:",omitempty"`
}
tests := []struct {
name string
val map[string][]rule
wire map[string][]RawMessage
}{
{
name: "nil",
val: nil,
wire: nil,
},
{
name: "empty",
val: map[string][]rule{},
wire: map[string][]RawMessage{},
},
{
name: "one",
val: map[string][]rule{
"foo": {{Ports: []int{1, 2, 3}}},
},
wire: map[string][]RawMessage{
"foo": {
`{"Ports":[1,2,3]}`,
},
},
},
{
name: "many",
val: map[string][]rule{
"foo": {{Ports: []int{1, 2, 3}}},
"bar": {{Ports: []int{4, 5, 6}}, {Ports: []int{7, 8, 9}}},
"baz": nil,
"abc": {},
"def": {{}},
},
wire: map[string][]RawMessage{
"foo": {
`{"Ports":[1,2,3]}`,
},
"bar": {
`{"Ports":[4,5,6]}`,
`{"Ports":[7,8,9]}`,
},
"baz": nil,
"abc": {},
"def": {"{}"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
j := must.Get(json.Marshal(tc.val))
var gotWire map[string][]RawMessage
if err := json.Unmarshal(j, &gotWire); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(gotWire, tc.wire) {
t.Errorf("got %#v; want %#v", gotWire, tc.wire)
}
j = must.Get(json.Marshal(tc.wire))
var gotVal map[string][]rule
if err := json.Unmarshal(j, &gotVal); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(gotVal, tc.val) {
t.Errorf("got %#v; want %#v", gotVal, tc.val)
}
})
}
}

View File

@@ -165,13 +165,19 @@ func (v NodeView) Online() *bool {
return &x
}
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
func (v NodeView) Expired() bool { return v.ж.Expired }
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) CapMap() views.MapFn[NodeCapability, []RawMessage, views.Slice[RawMessage]] {
return views.MapFnOf(v.ж.CapMap, func(t []RawMessage) views.Slice[RawMessage] {
return views.SliceOf(t)
})
}
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
func (v NodeView) Expired() bool { return v.ж.Expired }
func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
if v.ж.SelfNodeV4MasqAddrForThisPeer == nil {
return nil
@@ -180,7 +186,18 @@ func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
return &x
}
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
func (v NodeView) SelfNodeV6MasqAddrForThisPeer() *netip.Addr {
if v.ж.SelfNodeV6MasqAddrForThisPeer == nil {
return nil
}
x := *v.ж.SelfNodeV6MasqAddrForThisPeer
return &x
}
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
func (v NodeView) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers)
}
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
@@ -207,7 +224,8 @@ var _NodeViewNeedsRegeneration = Node(struct {
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []string
Capabilities []NodeCapability
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
@@ -215,7 +233,9 @@ var _NodeViewNeedsRegeneration = Node(struct {
DataPlaneAuditLogID string
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
SelfNodeV6MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// View returns a readonly view of Hostinfo.

View File

@@ -31,6 +31,7 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
var (
subcommand = ""
cc = "cc"
targetOS = env.Get("GOOS", nativeGOOS)
targetArch = env.Get("GOARCH", nativeGOARCH)
buildFlags = []string{"-trimpath"}
@@ -89,6 +90,22 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
// quoted in its entirety as a member of -ldflags. Source:
// https://github.com/golang/go/issues/6234
ldflags = append(ldflags, fmt.Sprintf("'-extldflags=%s'", strings.Join(extldflags, " ")))
case "windowsdll":
// Fake GOOS that translates to "windows, but building .dlls not .exes"
targetOS = "windows"
cgo = true
buildFlags = append(buildFlags, "-buildmode=c-shared")
ldflags = append(ldflags, "-H", "windows", "-s")
var mingwArch string
switch targetArch {
case "amd64":
mingwArch = "x86_64"
case "386":
mingwArch = "i686"
default:
return nil, nil, fmt.Errorf("unsupported GOARCH=%q when building with cgo", targetArch)
}
cc = fmt.Sprintf("%s-w64-mingw32-gcc", mingwArch)
case "windowsgui":
// Fake GOOS that translates to "windows, but building GUI .exes not console .exes"
targetOS = "windows"
@@ -166,7 +183,7 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
env.Set("CGO_ENABLED", boolStr(cgo))
env.Set("CGO_CFLAGS", strings.Join(cgoCflags, " "))
env.Set("CGO_LDFLAGS", strings.Join(cgoLdflags, " "))
env.Set("CC", "cc")
env.Set("CC", cc)
env.Set("TS_LINK_FAIL_REFLECT", boolStr(failReflect))
env.Set("GOROOT", goroot)

View File

@@ -27,6 +27,7 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
"tailscale.com/proxymap"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
@@ -45,7 +46,9 @@ type System struct {
Tun SubSystem[*tstun.Wrapper]
StateStore SubSystem[ipn.StateStore]
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
controlKnobs controlknobs.Knobs
controlKnobs controlknobs.Knobs
proxyMap proxymap.Mapper
}
// NetstackImpl is the interface that *netstack.Impl implements.
@@ -103,6 +106,11 @@ func (s *System) ControlKnobs() *controlknobs.Knobs {
return &s.controlKnobs
}
// ProxyMapper returns the ephemeral ip:port mapper.
func (s *System) ProxyMapper() *proxymap.Mapper {
return &s.proxyMap
}
// SubSystem represents some subsystem of the Tailscale node daemon.
//
// A subsystem can be set to a value, and then later retrieved. A subsystem

View File

@@ -408,7 +408,9 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
if nm == nil {
return
}
for _, addr := range nm.Addresses {
addrs := nm.GetAddresses()
for i := range addrs.LenIter() {
addr := addrs.At(i)
ip := addr.Addr()
if ip.Is6() {
ip6 = ip
@@ -509,7 +511,7 @@ func (s *Server) start() (reterr error) {
closePool.add(s.dialer)
sys.Set(eng)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get())
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}

View File

@@ -66,7 +66,7 @@ type Server struct {
// MapResponses sent to clients. It is keyed by the requesting nodes
// public key, and then the peer node's public key. The value is the
// masquerade address to use for that peer.
masquerades map[key.NodePublic]map[key.NodePublic]netip.Addr // node => peer => SelfNodeV4MasqAddrForThisPeer IP
masquerades map[key.NodePublic]map[key.NodePublic]netip.Addr // node => peer => SelfNodeV{4,6}MasqAddrForThisPeer IP
// suppressAutoMapResponses is the set of nodes that should not be sent
// automatic map responses from serveMap. (They should only get manually sent ones)
@@ -330,7 +330,7 @@ func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) {
// Node masquerades as for the Peer.
//
// Setting this will have future MapResponses for Node to have
// Peer.SelfNodeV4MasqAddrForThisPeer set to NodeMasqueradesAs.
// Peer.SelfNodeV{4,6}MasqAddrForThisPeer set to NodeMasqueradesAs.
// MapResponses for the Peer will now see Node.Addresses as
// NodeMasqueradesAs.
type MasqueradePair struct {
@@ -585,7 +585,7 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.
AllowedIPs: allowedIPs,
Hostinfo: req.Hostinfo.View(),
Name: req.Hostinfo.Hostname,
Capabilities: []string{
Capabilities: []tailcfg.NodeCapability{
tailcfg.CapabilityHTTPS,
tailcfg.NodeAttrFunnel,
tailcfg.CapabilityFunnelPorts + "?ports=8080,443",
@@ -889,7 +889,11 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
continue
}
if masqIP := nodeMasqs[p.Key]; masqIP.IsValid() {
p.SelfNodeV4MasqAddrForThisPeer = ptr.To(masqIP)
if masqIP.Is6() {
p.SelfNodeV6MasqAddrForThisPeer = ptr.To(masqIP)
} else {
p.SelfNodeV4MasqAddrForThisPeer = ptr.To(masqIP)
}
}
s.mu.Lock()

View File

@@ -8,6 +8,7 @@ package dnstype
import (
"net/netip"
"slices"
)
// Resolver is the configuration for one DNS resolver.
@@ -51,3 +52,15 @@ func (r *Resolver) IPPort() (ipp netip.AddrPort, ok bool) {
}
return
}
// Equal reports whether r and other are equal.
func (r *Resolver) Equal(other *Resolver) bool {
if r == nil || other == nil {
return r == other
}
if r == other {
return true
}
return r.Addr == other.Addr && slices.Equal(r.BootstrapResolution, other.BootstrapResolution)
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package dnstype
import (
"net/netip"
"reflect"
"slices"
"sort"
"testing"
)
func TestResolverEqual(t *testing.T) {
var fieldNames []string
for _, field := range reflect.VisibleFields(reflect.TypeOf(Resolver{})) {
fieldNames = append(fieldNames, field.Name)
}
sort.Strings(fieldNames)
if !slices.Equal(fieldNames, []string{"Addr", "BootstrapResolution"}) {
t.Errorf("Resolver fields changed; update test")
}
tests := []struct {
name string
a, b *Resolver
want bool
}{
{
name: "nil",
a: nil,
b: nil,
want: true,
},
{
name: "nil vs non-nil",
a: nil,
b: &Resolver{},
want: false,
},
{
name: "non-nil vs nil",
a: &Resolver{},
b: nil,
want: false,
},
{
name: "equal",
a: &Resolver{Addr: "dns.example.com"},
b: &Resolver{Addr: "dns.example.com"},
want: true,
},
{
name: "not equal addrs",
a: &Resolver{Addr: "dns.example.com"},
b: &Resolver{Addr: "dns2.example.com"},
want: false,
},
{
name: "not equal bootstrap",
a: &Resolver{
Addr: "dns.example.com",
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
},
b: &Resolver{
Addr: "dns.example.com",
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.4.4")},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.a.Equal(tt.b)
if got != tt.want {
t.Errorf("got %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -64,6 +64,7 @@ func (v ResolverView) Addr() string { return v.ж.Addr }
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
return views.SliceOf(v.ж.BootstrapResolution)
}
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ResolverViewNeedsRegeneration = Resolver(struct {

View File

@@ -33,16 +33,6 @@ type NetworkMap struct {
// It is the MapResponse.Node.Name value and ends with a period.
Name string
// Addresses is SelfNode.Addresses. (IP addresses of this Node directly)
//
// TODO(bradfitz): remove this field and make this a method.
Addresses []netip.Prefix
// MachineStatus is either tailcfg.MachineAuthorized or tailcfg.MachineUnauthorized,
// depending on SelfNode.MachineAuthorized.
// TODO(bradfitz): remove this field and make it a method.
MachineStatus tailcfg.MachineStatus
MachineKey key.MachinePublic
Peers []tailcfg.NodeView // sorted by Node.ID
@@ -96,6 +86,16 @@ func (nm *NetworkMap) User() tailcfg.UserID {
return 0
}
// GetAddresses returns the self node's addresses, or the zero value
// if SelfNode is invalid.
func (nm *NetworkMap) GetAddresses() views.Slice[netip.Prefix] {
var zero views.Slice[netip.Prefix]
if !nm.SelfNode.Valid() {
return zero
}
return nm.SelfNode.Addresses()
}
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
for _, p := range nm.Peers {
@@ -106,6 +106,17 @@ func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
return false
}
// GetMachineStatus returns the MachineStatus of the local node.
func (nm *NetworkMap) GetMachineStatus() tailcfg.MachineStatus {
if !nm.SelfNode.Valid() {
return tailcfg.MachineUnknown
}
if nm.SelfNode.MachineAuthorized() {
return tailcfg.MachineAuthorized
}
return tailcfg.MachineUnauthorized
}
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.
//
// If nm is nil or no peer is found, ok is false.
@@ -160,19 +171,27 @@ func MagicDNSSuffixOfNodeName(nodeName string) string {
//
// It will neither start nor end with a period.
func (nm *NetworkMap) MagicDNSSuffix() string {
if nm == nil {
return ""
}
return MagicDNSSuffixOfNodeName(nm.Name)
}
// SelfCapabilities returns SelfNode.Capabilities if nm and nm.SelfNode are
// non-nil. This is a method so we can use it in envknob/logknob without a
// circular dependency.
func (nm *NetworkMap) SelfCapabilities() views.Slice[string] {
var zero views.Slice[string]
func (nm *NetworkMap) SelfCapabilities() views.Slice[tailcfg.NodeCapability] {
var zero views.Slice[tailcfg.NodeCapability]
if nm == nil || !nm.SelfNode.Valid() {
return zero
}
out := nm.SelfNode.Capabilities().AsSlice()
nm.SelfNode.CapMap().Range(func(k tailcfg.NodeCapability, _ views.Slice[tailcfg.RawMessage]) (cont bool) {
out = append(out, k)
return true
})
return nm.SelfNode.Capabilities()
return views.SliceOf(out)
}
func (nm *NetworkMap) String() string {
@@ -211,7 +230,7 @@ func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ tailcfg.Node
// in equalConciseHeader in sync.
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
nm.NodeKey.ShortString(), nm.MachineStatus)
nm.NodeKey.ShortString(), nm.GetMachineStatus())
login := nm.UserProfiles[nm.User()].LoginName
if login == "" {
if nm.User().IsZero() {
@@ -221,25 +240,17 @@ func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
}
}
fmt.Fprintf(buf, " u=%s", login)
fmt.Fprintf(buf, " %v", nm.Addresses)
fmt.Fprintf(buf, " %v", nm.GetAddresses().AsSlice())
buf.WriteByte('\n')
}
// equalConciseHeader reports whether a and b are equal for the fields
// used by printConciseHeader.
func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
if a.NodeKey != b.NodeKey ||
a.MachineStatus != b.MachineStatus ||
a.User() != b.User() ||
len(a.Addresses) != len(b.Addresses) {
return false
}
for i, a := range a.Addresses {
if b.Addresses[i] != a {
return false
}
}
return true
return a.NodeKey == b.NodeKey &&
a.GetMachineStatus() == b.GetMachineStatus() &&
a.User() == b.User() &&
views.SliceEqual(a.GetAddresses(), b.GetAddresses())
}
// printPeerConcise appends to buf a line representing the peer p.

View File

@@ -4,6 +4,7 @@
package netmap
import (
"fmt"
"net/netip"
"reflect"
"slices"
@@ -11,6 +12,7 @@ import (
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
"tailscale.com/util/cmpx"
)
@@ -18,6 +20,7 @@ import (
// the change of a node's state.
type NodeMutation interface {
NodeIDBeingMutated() tailcfg.NodeID
Apply(*tailcfg.Node)
}
type mutatingNodeID tailcfg.NodeID
@@ -31,12 +34,24 @@ type NodeMutationDERPHome struct {
DERPRegion int
}
func (m NodeMutationDERPHome) Apply(n *tailcfg.Node) {
n.DERP = fmt.Sprintf("127.3.3.40:%v", m.DERPRegion)
}
// NodeMutation is a NodeMutation that says a node's endpoints have changed.
type NodeMutationEndpoints struct {
mutatingNodeID
Endpoints []netip.AddrPort
}
func (m NodeMutationEndpoints) Apply(n *tailcfg.Node) {
eps := make([]string, len(m.Endpoints))
for i, ep := range m.Endpoints {
eps[i] = ep.String()
}
n.Endpoints = eps
}
// NodeMutationOnline is a NodeMutation that says a node is now online or
// offline.
type NodeMutationOnline struct {
@@ -44,6 +59,10 @@ type NodeMutationOnline struct {
Online bool
}
func (m NodeMutationOnline) Apply(n *tailcfg.Node) {
n.Online = ptr.To(m.Online)
}
// NodeMutationLastSeen is a NodeMutation that says a node's LastSeen
// value should be set to the current time.
type NodeMutationLastSeen struct {
@@ -51,6 +70,10 @@ type NodeMutationLastSeen struct {
LastSeen time.Time
}
func (m NodeMutationLastSeen) Apply(n *tailcfg.Node) {
n.LastSeen = ptr.To(m.LastSeen)
}
var peerChangeFields = sync.OnceValue(func() []reflect.StructField {
var fields []reflect.StructField
rt := reflect.TypeOf((*tailcfg.PeerChange)(nil)).Elem()

View File

@@ -276,6 +276,16 @@ func SliceContains[T comparable](v Slice[T], e T) bool {
return false
}
// SliceContainsFunc reports whether f reports true for any element in v.
func SliceContainsFunc[T any](v Slice[T], f func(T) bool) bool {
for i := 0; i < v.Len(); i++ {
if f(v.At(i)) {
return true
}
}
return false
}
// SliceEqual is like the standard library's slices.Equal, but for two views.
func SliceEqual[T comparable](a, b Slice[T]) bool {
return slices.Equal(a.ж, b.ж)

View File

@@ -124,6 +124,8 @@ func TestViewUtils(t *testing.T) {
c.Check(v.IndexFunc(func(s string) bool { return strings.HasPrefix(s, "z") }), qt.Equals, -1)
c.Check(SliceContains(v, "bar"), qt.Equals, true)
c.Check(SliceContains(v, "baz"), qt.Equals, false)
c.Check(SliceContainsFunc(v, func(s string) bool { return strings.HasPrefix(s, "f") }), qt.Equals, true)
c.Check(SliceContainsFunc(v, func(s string) bool { return len(s) > 3 }), qt.Equals, false)
c.Check(SliceEqualAnyOrder(v, v), qt.Equals, true)
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"bar", "foo"})), qt.Equals, true)
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"foo"})), qt.Equals, false)

View File

@@ -174,6 +174,9 @@ func (c *Cache[K, V]) deleteElement(ent *entry[K, V]) {
} else {
ent.next.prev = ent.prev
ent.prev.next = ent.next
if c.head == ent {
c.head = ent.next
}
}
delete(c.lookup, ent.key)
}

View File

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
xmaps "golang.org/x/exp/maps"
)
func TestLRU(t *testing.T) {
@@ -48,6 +49,156 @@ func TestLRU(t *testing.T) {
}
}
func TestLRUDeleteCorruption(t *testing.T) {
// Regression test for tailscale/corp#14747
c := Cache[int, bool]{}
c.Set(1, true)
c.Set(2, true) // now 2 is the head
c.Delete(2) // delete the head
c.check(t)
}
func TestStressEvictions(t *testing.T) {
const (
cacheSize = 1_000
numKeys = 10_000
numProbes = 100_000
)
vm := map[uint64]bool{}
for len(vm) < numKeys {
vm[rand.Uint64()] = true
}
vals := xmaps.Keys(vm)
c := Cache[uint64, bool]{
MaxEntries: cacheSize,
}
for i := 0; i < numProbes; i++ {
v := vals[rand.Intn(len(vals))]
c.Set(v, true)
if l := c.Len(); l > cacheSize {
t.Fatalf("Cache size now %d, want max %d", l, cacheSize)
}
}
}
func TestStressBatchedEvictions(t *testing.T) {
// One of Cache's consumers dynamically adjusts the cache size at
// runtime, and does batched evictions as needed. This test
// simulates that behavior.
const (
cacheSizeMin = 1_000
cacheSizeMax = 2_000
numKeys = 10_000
numProbes = 100_000
)
vm := map[uint64]bool{}
for len(vm) < numKeys {
vm[rand.Uint64()] = true
}
vals := xmaps.Keys(vm)
c := Cache[uint64, bool]{}
for i := 0; i < numProbes; i++ {
v := vals[rand.Intn(len(vals))]
c.Set(v, true)
if c.Len() == cacheSizeMax {
// Batch eviction down to cacheSizeMin
for c.Len() > cacheSizeMin {
c.DeleteOldest()
}
}
if l := c.Len(); l > cacheSizeMax {
t.Fatalf("Cache size now %d, want max %d", l, cacheSizeMax)
}
}
}
func TestLRUStress(t *testing.T) {
var c Cache[int, int]
const (
maxSize = 500
numProbes = 5_000
)
for i := 0; i < numProbes; i++ {
n := rand.Intn(maxSize * 2)
op := rand.Intn(4)
switch op {
case 0:
c.Get(n)
case 1:
c.Set(n, n)
case 2:
c.Delete(n)
case 3:
for c.Len() > maxSize {
c.DeleteOldest()
}
}
c.check(t)
}
}
// check verifies that c.lookup and c.head are consistent in size with
// each other, and that the ring has the same size when traversed in
// both directions.
func (c *Cache[K, V]) check(t testing.TB) {
size := c.Len()
nextLen := c.nextLen(t, size)
prevLen := c.prevLen(t, size)
if nextLen != size {
t.Fatalf("next list len %v != map len %v", nextLen, size)
}
if prevLen != size {
t.Fatalf("prev list len %v != map len %v", prevLen, size)
}
}
// nextLen returns the length of the ring at c.head when traversing
// the .next pointers.
func (c *Cache[K, V]) nextLen(t testing.TB, limit int) (n int) {
if c.head == nil {
return 0
}
n = 1
at := c.head.next
for at != c.head {
limit--
if limit < 0 {
t.Fatal("next list is too long")
}
n++
at = at.next
}
return n
}
// prevLen returns the length of the ring at c.head when traversing
// the .prev pointers.
func (c *Cache[K, V]) prevLen(t testing.TB, limit int) (n int) {
if c.head == nil {
return 0
}
n = 1
at := c.head.prev
for at != c.head {
limit--
if limit < 0 {
t.Fatal("next list is too long")
}
n++
at = at.prev
}
return n
}
func TestDumpHTML(t *testing.T) {
c := Cache[int, string]{MaxEntries: 3}

View File

@@ -6,7 +6,6 @@
package filter
import (
"encoding/json"
"net/netip"
"tailscale.com/tailcfg"
@@ -24,9 +23,11 @@ func (src *Match) Clone() *Match {
dst.IPProto = append(src.IPProto[:0:0], src.IPProto...)
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
dst.Caps = make([]CapMatch, len(src.Caps))
for i := range dst.Caps {
dst.Caps[i] = *src.Caps[i].Clone()
if src.Caps != nil {
dst.Caps = make([]CapMatch, len(src.Caps))
for i := range dst.Caps {
dst.Caps[i] = *src.Caps[i].Clone()
}
}
return dst
}
@@ -47,10 +48,7 @@ func (src *CapMatch) Clone() *CapMatch {
}
dst := new(CapMatch)
*dst = *src
dst.Values = make([]json.RawMessage, len(src.Values))
for i := range dst.Values {
dst.Values[i] = append(src.Values[i][:0:0], src.Values[i]...)
}
dst.Values = append(src.Values[:0:0], src.Values...)
return dst
}
@@ -58,5 +56,5 @@ func (src *CapMatch) Clone() *CapMatch {
var _CapMatchCloneNeedsRegeneration = CapMatch(struct {
Dst netip.Prefix
Cap tailcfg.PeerCapability
Values []json.RawMessage
Values []tailcfg.RawMessage
}{})

View File

@@ -873,7 +873,7 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
}
}
func TestCaps(t *testing.T) {
func TestPeerCaps(t *testing.T) {
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
{
SrcIPs: []string{"*"},

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