Compare commits

...

58 Commits

Author SHA1 Message Date
Irbe Krumina
d8dda0048e wip
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-13 17:50:45 +00:00
Irbe Krumina
e38522c081 go.{mod,sum},build_docker.sh: bump mkctr, add ability to set OCI annotations for images (#14065)
Updates tailscale/tailscale#12914

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-12 14:23:38 +00:00
Tom Proctor
d8a3683fdf cmd/k8s-operator: restart ProxyGroup pods less (#14045)
We currently annotate pods with a hash of the tailscaled config so that
we can trigger pod restarts whenever it changes. However, the hash
updates more frequently than is necessary causing more restarts than is
necessary. This commit removes two causes; scaling up/down and removing
the auth key after pods have initially authed to control. However, note
that pods will still restart on scale-up/down because of the updated set
of volumes mounted into each pod. Hopefully we can fix that in a planned
follow-up PR.

Updates #13406

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2024-11-12 14:18:19 +00:00
Brad Fitzpatrick
4e0fc037e6 all: use iterators over slice views more
This gets close to all of the remaining ones.

Updates #12912

Change-Id: I9c672bbed2654a6c5cab31e0cbece6c107d8c6fa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-11 13:22:34 -08:00
Brad Fitzpatrick
00be1761b7 util/codegen: treat unique.Handle as an opaque value type
It doesn't need a Clone method, like a time.Time, etc.

And then, because Go 1.23+ uses unique.Handle internally for
the netip package types, we can remove those special cases.

Updates #14058 (pulled out from that PR)
Updates tailscale/corp#24485

Change-Id: Iac3548a9417ccda5987f98e0305745a6e178b375
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-11 12:39:19 -08:00
Irbe Krumina
b9ecc50ce3 cmd/k8s-operator,k8s-operator,kube/kubetypes: add an option to configure app connector via Connector spec (#13950)
* cmd/k8s-operator,k8s-operator,kube/kubetypes: add an option to configure app connector via Connector spec

Updates tailscale/tailscale#11113

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-11 11:43:54 +00:00
M. J. Fromberger
6ff85846bc safeweb: add a Shutdown method to the Server type (#14048)
Updates #14047

Change-Id: I2d20454c715b11ad9c6aad1d81445e05a170c3a2
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2024-11-08 10:02:16 -08:00
Anton Tolchanov
64d70fb718 ipn/ipnlocal: log a summary of posture identity response
Perhaps I was too opimistic in #13323 thinking we won't need logs for
this. Let's log a summary of the response without logging specific
identifiers.

Updates tailscale/corp#24437

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-11-08 16:20:07 +00:00
Brad Fitzpatrick
020cacbe70 derp/derphttp: don't link websockets other than on GOOS=js
Or unless the new "ts_debug_websockets" build tag is set.

Updates #1278

Change-Id: Ic4c4f81c1924250efd025b055585faec37a5491d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 22:29:41 -08:00
Brad Fitzpatrick
c3306bfd15 control/controlhttp/controlhttpserver: split out Accept to its own package
Otherwise all the clients only using control/controlhttp for the
ts2021 HTTP client were also pulling in WebSocket libraries, as the
server side always needs to speak websockets, but only GOOS=js clients
speak it.

This doesn't yet totally remove the websocket dependency on Linux because
Linux has a envknob opt-in to act like GOOS=js for manual testing and force
the use of WebSockets for DERP only (not control). We can put that behind
a build tag in a future change to eliminate the dep on all GOOSes.

Updates #1278

Change-Id: I4f60508f4cad52bf8c8943c8851ecee506b7ebc9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 22:29:41 -08:00
Brad Fitzpatrick
23880eb5b0 cmd/tailscaled: support "ts_omit_ssh" build tag to remove SSH
Some environments would like to remove Tailscale SSH support for the
binary for various reasons when not needed (either for peace of mind,
or the ~1MB of binary space savings).

Updates tailscale/corp#24454
Updates #1278
Updates #12614

Change-Id: Iadd6c5a393992c254b5dc9aa9a526916f96fd07a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 16:06:59 -08:00
Irbe Krumina
2c8859c2e7 client/tailscale,ipn/{ipnlocal,localapi}: add a pre-shutdown localAPI endpoint that terminates control connections. (#14028)
Adds a /disconnect-control local API endpoint that just shuts down control client.
This can be run before shutting down an HA subnet router/app connector replica - it will ensure
that all connection to control are dropped and control thus considers this node inactive and tells
peers to switch over to another replica. Meanwhile the existing connections keep working (assuming
that the replica is given some graceful shutdown period).

Updates tailscale/tailscale#14020

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-07 19:27:53 +00:00
Brad Fitzpatrick
3090461961 tsweb/varz: optimize some allocs, add helper func for others
Updates #cleanup
Updates tailscale/corp#23546 (noticed when doing this)

Change-Id: Ia9f627fe32bb4955739b2787210ba18f5de27f4d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-07 08:09:16 -08:00
Irbe Krumina
8ba9b558d2 envknob,kube/kubetypes,cmd/k8s-operator: add app type for ProxyGroup (#14029)
Sets a custom hostinfo app type for ProxyGroup replicas, similarly
to how we do it for all other Kubernetes Operator managed components.

Updates tailscale/tailscale#13406,tailscale/corp#22920

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-07 12:42:29 +00:00
Percy Wegmann
8dcbd988f7 cmd/derper: show more information on home page
- Basic description of DERP

If configured to do so, also show

- Mailto link to security@tailscale.com
- Link to Tailscale Security Policies
- Link to Tailscale Acceptable Use Policy

Updates tailscale/corp#24092

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-11-06 11:06:08 -06:00
License Updater
065825e94c licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-11-05 15:33:17 -08:00
Brad Fitzpatrick
01185e436f types/result, util/lineiter: add package for a result type, use it
This adds a new generic result type (motivated by golang/go#70084) to
try it out, and uses it in the new lineutil package (replacing the old
lineread package), changing that package to return iterators:
sometimes over []byte (when the input is all in memory), but sometimes
iterators over results of []byte, if errors might happen at runtime.

Updates #12912
Updates golang/go#70084

Change-Id: Iacdc1070e661b5fb163907b1e8b07ac7d51d3f83
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-05 10:27:52 -08:00
Irbe Krumina
809a6eba80 cmd/k8s-operator: allow to optionally configure tailscaled port (#14005)
Updates tailscale/tailscale#13981

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-11-04 18:42:51 +00:00
Brad Fitzpatrick
d4222fae95 tsnet: add accessor to get tsd.System
Pulled of otherwise unrelated PR #13884.

Updates tailscale/corp#22075

Change-Id: I5b539fcb4aca1b93406cf139c719a5e3c64ff7f7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 09:58:38 -08:00
Brad Fitzpatrick
45da3a4b28 cmd/tsconnect: block after starting esbuild dev server
Thanks to @davidbuzz for raising the issue in #13973.

Fixes #8272
Fixes #13973

Change-Id: Ic413e14d34c82df3c70a97e591b90316b0b4946b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-03 07:30:22 -08:00
VimT
43138c7a5c net/socks5: optimize UDP relay
Key changes:
- No mutex for every udp package: replace syncs.Map with regular map for udpTargetConns
- Use socksAddr as map key for better type safety
- Add test for multi udp target

Updates #7581

Change-Id: Ic3d384a9eab62dcbf267d7d6d268bf242cc8ed3c
Signed-off-by: VimT <me@vimt.me>
2024-11-01 15:47:52 -07:00
VimT
b0626ff84c net/socks5: fix UDP relay in userspace-networking mode
This commit addresses an issue with the SOCKS5 UDP relay functionality
when using the --tun=userspace-networking option. Previously, UDP packets
were not being correctly routed into the Tailscale network in this mode.

Key changes:
- Replace single UDP connection with a map of connections per target
- Use c.srv.dial for creating connections to ensure proper routing

Updates #7581

Change-Id: Iaaa66f9de6a3713218014cf3f498003a7cac9832
Signed-off-by: VimT <me@vimt.me>
2024-11-01 15:47:52 -07:00
Brad Fitzpatrick
634cc2ba4a wgengine/netstack: remove unused taildrive deps
A filesystem was plumbed into netstack in 993acf4475
but hasn't been used since 2d5d6f5403. Remove it.

Noticed while rebasing a Tailscale fork elsewhere.

Updates tailscale/corp#16827

Change-Id: Ib76deeda205ffe912b77a59b9d22853ebff42813
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-11-01 13:40:46 -07:00
Maisem Ali
d09e9d967f ipn/ipnlocal: reload prefs correctly on ReloadConfig
We were only updating the ProfileManager and not going down
the EditPrefs path which meant the prefs weren't applied
till either the process restarted or some other pref changed.

This makes it so that we reconfigure everything correctly when
ReloadConfig is called.

Updates #13032

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-11-01 13:37:46 -07:00
Renato Aguiar
0ffc7bf38b Fix MagicDNS on OpenBSD
Add OpenBSD to the list of platforms that need DNS reconfigured on link changes.

Signed-off-by: Renato Aguiar <renato@renatoaguiar.net>
2024-11-01 10:44:30 -07:00
Jordan Whited
49de23cf1b net/netcheck: add addReportHistoryAndSetPreferredDERP() test case (#13989)
Add an explicit case for exercising preferred DERP hysteresis around
the branch that compares latencies on a percentage basis.

Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-10-31 19:25:00 -07:00
Aaron Klotz
84c8860472 util/syspolicy: add policy key for onboarding flow visibility
Updates https://github.com/tailscale/corp/issues/23789

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-10-31 15:46:40 -06:00
Andrew Lytvynov
ddbc950f46 safeweb: add support for custom CSP (#13975)
To allow more flexibility with CSPs, add a fully customizable `CSP` type
that can be provided in `Config` and encodes itself into the correct
format. Preserve the `CSPAllowInlineStyles` option as is today, but
maybe that'll get deprecated later in favor of the new CSP field.

In particular, this allows for pages loading external JS, or inline JS
with nonces or hashes (see
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_inline_script)

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

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-10-31 12:13:29 -07:00
Andrea Gottardo
6985369479 net/sockstats: prevent crash in setNetMon (#13985) 2024-10-31 12:00:34 -07:00
Andrew Lytvynov
3477bfd234 safeweb: add support for "/" and "/foo" handler distinction (#13980)
By counting "/" elements in the pattern we catch many scenarios, but not
the root-level handler. If either of the patterns is "/", compare the
pattern length to pick the right one.

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

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-10-31 11:12:38 -07:00
Nick Khyl
3f626c0d77 cmd/tailscale/cli, client/tailscale, ipn/localapi: add tailscale syspolicy {list,reload} commands
In this PR, we add the tailscale syspolicy command with two subcommands: list, which displays
policy settings, and reload, which forces a reload of those settings. We also update the LocalAPI
and LocalClient to facilitate these additions.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-31 10:53:43 -05:00
Irbe Krumina
45354dab9b ipn,tailcfg: add app connector config knob to conffile (#13942)
Make it possible to advertise app connector via a new conffile field.
Also bumps capver - conffile deserialization errors out if unknonw
fields are set, so we need to know which clients understand the new field.

Updates tailscale/tailscale#11113

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-10-31 14:45:57 +00:00
Anton Tolchanov
b4f46c31bb wgengine/magicsock: export packet drop metric for outbound errors
This required sharing the dropped packet metric between two packages
(tstun and magicsock), so I've moved its definition to util/usermetric.

Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-31 08:33:24 +00:00
Anton Tolchanov
532b26145a wgengine/magicsock: exclude disco from throughput metrics
The user-facing metrics are intended to track data transmitted at
the overlay network level.

Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-31 08:01:19 +00:00
James Tucker
e1e22785b4 net/netcheck: ensure prior preferred DERP is always in netchecks
In an environment with unstable latency, such as upstream bufferbloat,
there are cases where a full netcheck could drop the prior preferred
DERP (likely home DERP) from future netcheck probe plans. This will then
likely result in a home DERP having a missing sample on the next
incremental netcheck, ultimately resulting in a home DERP move.

This change does not fix our overall response to highly unstable
latency, but it is an incremental improvement to prevent single spurious
samples during a full netcheck from alone triggering a flapping
condition, as now the prior changes to include historical latency will
still provide the desired resistance, and the home DERP should not move
unless latency is consistently worse over a 5 minute period.

Note that there is a nomenclature and semantics issue remaining in the
difference between a report preferred DERP and a home DERP. A report
preferred DERP is aspirational, it is what will be picked as a home DERP
if a home DERP connection needs to be established. A nodes home DERP may
be different than a recent preferred DERP, in which case a lot of
netcheck logic is fallible. In future enhancements much of the DERP move
logic should move to consider the home DERP, rather than recent report
preferred DERP.

Updates #8603
Updates #13969

Signed-off-by: James Tucker <james@tailscale.com>
2024-10-30 17:19:26 -07:00
Brad Fitzpatrick
f81348a16b util/syspolicy/source: put EnvPolicyStore env keys in their own namespace
... all prefixed with TS_DEBUGSYSPOLICY_*.

Updates #13193
Updates #12687
Updates #13855

Change-Id: Ia8024946f53e2b3afda4456a7bb85bbcf6d12bfc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-10-30 11:27:27 -07:00
Nick Khyl
540e4c83d0 util/syspolicy/setting: make setting.Snapshot JSON-marshallable
We make setting.Snapshot JSON-marshallable in preparation for returning it from the LocalAPI.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-30 12:50:29 -05:00
Nick Khyl
2a2228f97b util/syspolicy/setting: make setting.RawItem JSON-marshallable
We add setting.RawValue, a new type that facilitates unmarshalling JSON numbers and arrays
as uint64 and []string (instead of float64 and []any) for policy setting values.
We then use it to make setting.RawItem JSON-marshallable and update the tests.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-30 12:50:29 -05:00
Nick Khyl
2cc1100d24 util/syspolicy/source: use errors instead of github.com/pkg/errors
Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-30 12:14:36 -05:00
Nick Khyl
2336c340c4 util/syspolicy: implement a syspolicy store that reads settings from environment variables
In this PR, we implement (but do not use yet, pending #13727 review) a syspolicy/source.Store
that reads policy settings from environment variables. It converts a CamelCase setting.Key,
such as AuthKey or ExitNodeID, to a SCREAMING_SNAKE_CASE, TS_-prefixed environment
variable name, such as TS_AUTH_KEY and TS_EXIT_NODE_ID. It then looks up the variable
and attempts to parse it according to the expected value type. If the environment variable
is not set, the policy setting is considered not configured in this store (the syspolicy package
will still read it from other sources). Similarly, if the environment variable has an invalid value
for the setting type, it won't be used (though the reported/logged error will differ).

Updates #13193
Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-30 11:12:22 -05:00
Irbe Krumina
1103044598 cmd/k8s-operator,k8s-operator: add topology spread constraints to ProxyClass (#13959)
Now when we have HA for egress proxies, it makes sense to support topology
spread constraints that would allow users to define more complex
topologies of how proxy Pods need to be deployed in relation with other
Pods/across regions etc.

Updates tailscale/tailscale#13406

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-10-30 10:45:31 +00:00
Tim Walters
856ea2376b wgengine/magicsock: log home DERP changes with latency
This adds additional logging on DERP home changes to allow
better troubleshooting.

Updates tailscale/corp#18095

Signed-off-by: Tim Walters <tim@tailscale.com>
2024-10-29 16:05:41 -04:00
Jonathan Nobels
aecb0ab76b tstest/tailmac: add support for mounting host directories in the guest (#13957)
updates tailscale/corp#24197

tailmac run now supports the --share option which will allow you
to specify a directory on the host which can be mounted in the guest
using  mount_virtiofs vmshare <path>.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-10-29 13:49:51 -04:00
Jonathan Nobels
0f9a054cba tstest/tailmac: fix Host.app path generation (#13953)
updates tailscale/corp#24197

Generation of the Host.app path was erroneous and tailmac run
would not work unless the pwd was tailmac/bin.  Now you can
be able to invoke tailmac from anywhere.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-10-29 13:49:29 -04:00
Anton Tolchanov
9545e36007 cmd/tailscale/cli: add 'tailscale metrics' command
- `tailscale metrics print`: to show metric values in console
- `tailscale metrics write`: to write metrics to a file (with a tempfile
  & rename dance, which is atomic on Unix).

Also, remove the `TS_DEBUG_USER_METRICS` envknob as we are getting
more confident in these metrics.

Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-29 15:08:36 +00:00
Anton Tolchanov
38af62c7b3 ipn/ipnlocal: remove the primary routes gauge for now
Not confident this is the right way to expose this, so let's remote it
for now.

Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-29 15:07:54 +00:00
Anton Tolchanov
11e96760ff wgengine/magicsock: fix stats packet counter on derp egress
Updates tailscale/corp#22075

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-29 15:07:45 +00:00
Anton Tolchanov
94fa6d97c5 ipn/ipnlocal: log errors while fetching serial numbers
If the client cannot fetch a serial number, write a log message helping
the user understand what happened. Also, don't just return the error
immediately, since we still have a chance to collect network interface
addresses.

Updates #5902

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-10-29 14:36:08 +00:00
James Tucker
0d76d7d21c tool/gocross: remove trimpath from test builds
trimpath can be inconvenient for IDEs and LSPs that do not always
correctly handle module relative paths, and can also contribute to
caching bugs taking effect. We rarely have a real need for trimpath of
test produced binaries, so avoiding it should be a net win.

Updates #2988
Signed-off-by: James Tucker <james@tailscale.com>
2024-10-28 16:10:55 -07:00
James Tucker
c0a1ed86cb tstest/natlab: add latency & loss simulation
A simple implementation of latency and loss simulation, applied to
writes to the ethernet interface of the NIC. The latency implementation
could be optimized substantially later if necessary.

Updates #13355
Signed-off-by: James Tucker <james@tailscale.com>
2024-10-28 12:49:56 -07:00
License Updater
41aac26106 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-10-28 08:38:18 -07:00
Renato Aguiar
5d07c17b93 net/dns: fix blank lines being added to resolv.conf on OpenBSD (#13928)
During resolv.conf update, old 'search' lines are cleared but '\n' is not
deleted, leaving behind a new blank line on every update.

This adds 's' flag to regexp, so '\n' is included in the match and deleted when
old lines are cleared.

Also, insert missing `\n` when updated 'search' line is appended to resolv.conf.

Signed-off-by: Renato Aguiar <renato@renatoaguiar.net>
2024-10-28 08:00:48 -07:00
Irbe Krumina
9d1348fe21 ipn/store/kubestore: don't error if state cannot be preloaded (#13926)
Preloading of state from kube Secret should not
error if the Secret does not exist.

Updates tailscale/tailscale#7671

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-10-27 15:54:38 +00:00
Irbe Krumina
853fe3b713 ipn/store/kubestore: cache state in memory (#13918)
Cache state in memory on writes, read from memory
in reads.
kubestore was previously always reading state from a Secret.
This change should fix bugs caused by temporary loss of access
to kube API server and imporove overall performance

Fixes #7671
Updates tailscale/tailscale#12079,tailscale/tailscale#13900

Signed-off-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Maisem Ali <maisem@tailscale.com>
2024-10-26 09:33:47 -05:00
Nick Kirby
6ab39b7bcd cmd/k8s-operator: validate that tailscale.com/tailnet-ip annotation value is a valid IP
Fixes #13836
Signed-off-by: Nick Kirby <nrkirb@gmail.com>
2024-10-26 13:03:36 +01:00
Nick Khyl
e815ae0ec4 util/syspolicy, ipn/ipnlocal: update syspolicy package to utilize syspolicy/rsop
In this PR, we update the syspolicy package to utilize syspolicy/rsop under the hood,
and remove syspolicy.CachingHandler, syspolicy.windowsHandler and related code
which is no longer used.

We mark the syspolicy.Handler interface and RegisterHandler/SetHandlerForTest functions
as deprecated, but keep them temporarily until they are no longer used in other repos.

We also update the package to register setting definitions for all existing policy settings
and to register the Registry-based, Windows-specific policy stores when running on Windows.

Finally, we update existing internal and external tests to use the new API and add a few more
tests and benchmarks.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-10-25 12:41:07 -05:00
Andrew Dunham
7fe6e50858 net/dns/resolver: fix test flake
Updates #13902

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib2def19caad17367e9a31786ac969278e65f51c6
2024-10-24 13:36:57 -05:00
Paul Scott
212270463b cmd/testwrapper: add pkg runtime to output (#13894)
Fixes #13893

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-10-24 09:41:54 -05:00
149 changed files with 4810 additions and 1902 deletions

View File

@@ -17,12 +17,20 @@ eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.18"
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
# annotations defined here will be overriden by release scripts that call this script.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
DEFAULT_ANNOTATIONS="org.opencontainers.image.source=https://github.com/tailscale/tailscale/blob/main/build_docker.sh,org.opencontainers.image.vendor=Tailscale"
PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
# OCI annotations that will be added to the image.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}"
case "$TARGET" in
client)
@@ -43,6 +51,7 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/containerboot
;;
operator)
@@ -60,6 +69,7 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/operator
;;
k8s-nameserver)
@@ -77,6 +87,7 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/k8s-nameserver
;;
*)

View File

@@ -40,6 +40,7 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
"tailscale.com/util/syspolicy/setting"
)
// defaultLocalClient is the default LocalClient when using the legacy
@@ -814,6 +815,33 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
return decodeJSON[*ipn.Prefs](body)
}
// GetEffectivePolicy returns the effective policy for the specified scope.
func (lc *LocalClient) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// ReloadEffectivePolicy reloads the effective policy for the specified scope
// by reading and merging policy settings from all applicable policy sources.
func (lc *LocalClient) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// GetDNSOSConfig returns the system DNS configuration for the current device.
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
@@ -1299,6 +1327,17 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
return nil
}
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
// to another replica whilst there is still some grace period for the existing connections to terminate.
func (lc *LocalClient) DisconnectControl(ctx context.Context) error {
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
if err != nil {
return fmt.Errorf("error disconnecting control: %w", err)
}
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {

View File

@@ -116,7 +116,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
tailscale.com/net/wsconn from tailscale.com/cmd/derper
tailscale.com/paths from tailscale.com/client/tailscale
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
@@ -140,6 +140,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/ipn
tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/ipn+
@@ -154,7 +155,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/fastuuid from tailscale.com/tsweb
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health+
@@ -164,11 +165,16 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/syspolicy from tailscale.com/ipn
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/testenv from tailscale.com/util/syspolicy+
tailscale.com/util/usermetric from tailscale.com/health
tailscale.com/util/vizerror from tailscale.com/tailcfg+
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil/gp from tailscale.com/util/syspolicy/source
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/envknob+
@@ -189,7 +195,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
W golang.org/x/exp/constraints from tailscale.com/util/winutil
golang.org/x/exp/maps from tailscale.com/util/syspolicy/setting
golang.org/x/exp/maps from tailscale.com/util/syspolicy/setting+
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
@@ -250,7 +256,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
encoding/pem from crypto/tls+
errors from bufio+
expvar from github.com/prometheus/client_golang/prometheus+
flag from tailscale.com/cmd/derper
flag from tailscale.com/cmd/derper+
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
@@ -258,6 +264,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
html/template from tailscale.com/cmd/derper
io from bufio+
io/fs from crypto/x509+
io/ioutil from github.com/mitchellh/go-ps+
@@ -284,7 +291,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/derper
W os/user from tailscale.com/util/winutil
W os/user from tailscale.com/util/winutil+
path from github.com/prometheus/client_golang/prometheus/internal+
path/filepath from crypto/x509+
reflect from crypto/x509+
@@ -302,6 +309,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from crypto/x509+

View File

@@ -19,6 +19,7 @@ import (
"expvar"
"flag"
"fmt"
"html/template"
"io"
"log"
"math"
@@ -212,25 +213,16 @@ func main() {
tsweb.AddBrowserHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
io.WriteString(w, `<html><body>
<h1>DERP</h1>
<p>
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
</p>
<p>
Documentation:
</p>
<ul>
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
</ul>
`)
if !*runDERP {
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
}
if tsweb.AllowDebugAccess(r) {
io.WriteString(w, "<p>Debug info at <a href='/debug/'>/debug/</a>.</p>\n")
err := homePageTemplate.Execute(w, templateData{
ShowAbuseInfo: validProdHostname.MatchString(*hostname),
Disabled: !*runDERP,
AllowDebug: tsweb.AllowDebugAccess(r),
})
if err != nil {
if r.Context().Err() == nil {
log.Printf("homePageTemplate.Execute: %v", err)
}
return
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -468,3 +460,52 @@ func init() {
return 0
}))
}
type templateData struct {
ShowAbuseInfo bool
Disabled bool
AllowDebug bool
}
// homePageTemplate renders the home page using [templateData].
var homePageTemplate = template.Must(template.New("home").Parse(`<html><body>
<h1>DERP</h1>
<p>
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
</p>
<p>
It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic
for Tailscale clients.
</p>
{{if .ShowAbuseInfo }}
<p>
If you suspect abuse, please contact <a href="mailto:security@tailscale.com">security@tailscale.com</a>.
</p>
{{end}}
<p>
Documentation:
</p>
<ul>
{{if .ShowAbuseInfo }}
<li><a href="https://tailscale.com/security-policies">Tailscale Security Policies</a></li>
<li><a href="https://tailscale.com/tailscale-aup">Tailscale Acceptable Use Policies</a></li>
{{end}}
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
</ul>
{{if .Disabled}}
<p>Status: <b>disabled</b></p>
{{end}}
{{if .AllowDebug}}
<p>Debug info at <a href='/debug/'>/debug/</a>.</p>
{{end}}
</body>
</html>
`))

View File

@@ -4,7 +4,9 @@
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -110,3 +112,30 @@ func TestDeps(t *testing.T) {
},
}.Check(t)
}
func TestTemplate(t *testing.T) {
buf := &bytes.Buffer{}
err := homePageTemplate.Execute(buf, templateData{
ShowAbuseInfo: true,
Disabled: true,
AllowDebug: true,
})
if err != nil {
t.Fatal(err)
}
str := buf.String()
if !strings.Contains(str, "If you suspect abuse") {
t.Error("Output is missing abuse mailto")
}
if !strings.Contains(str, "Tailscale Security Policies") {
t.Error("Output is missing Tailscale Security Policies link")
}
if !strings.Contains(str, "Status:") {
t.Error("Output is missing disabled status")
}
if !strings.Contains(str, "Debug info") {
t.Error("Output is missing debug info")
}
fmt.Println(buf.String())
}

View File

@@ -13,7 +13,8 @@ import (
"sync"
"time"
"github.com/pkg/errors"
"errors"
"go.uber.org/zap"
xslices "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
@@ -58,6 +59,7 @@ type ConnectorReconciler struct {
subnetRouters set.Slice[types.UID] // for subnet routers gauge
exitNodes set.Slice[types.UID] // for exit nodes gauge
appConnectors set.Slice[types.UID] // for app connectors gauge
}
var (
@@ -67,6 +69,8 @@ var (
gaugeConnectorSubnetRouterResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithSubnetRouterCount)
// gaugeConnectorExitNodeResources tracks the number of Connectors currently managed by this operator instance that are exit nodes.
gaugeConnectorExitNodeResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithExitNodeCount)
// gaugeConnectorAppConnectorResources tracks the number of Connectors currently managed by this operator instance that are app connectors.
gaugeConnectorAppConnectorResources = clientmetric.NewGauge(kubetypes.MetricConnectorWithAppConnectorCount)
)
func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
@@ -108,13 +112,12 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
oldCnStatus := cn.Status.DeepCopy()
setStatus := func(cn *tsapi.Connector, _ tsapi.ConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, status, reason, message, cn.Generation, a.clock, logger)
var updateErr error
if !apiequality.Semantic.DeepEqual(oldCnStatus, cn.Status) {
// An error encountered here should get returned by the Reconcile function.
if updateErr := a.Client.Status().Update(ctx, cn); updateErr != nil {
err = errors.Wrap(err, updateErr.Error())
}
updateErr = a.Client.Status().Update(ctx, cn)
}
return res, err
return res, errors.Join(err, updateErr)
}
if !slices.Contains(cn.Finalizers, FinalizerName) {
@@ -150,6 +153,9 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque
cn.Status.SubnetRoutes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
if cn.Spec.AppConnector != nil {
cn.Status.IsAppConnector = true
}
cn.Status.SubnetRoutes = ""
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
@@ -189,23 +195,37 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge
sts.Connector.routes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
}
if cn.Spec.AppConnector != nil {
sts.Connector.isAppConnector = true
if len(cn.Spec.AppConnector.Routes) != 0 {
sts.Connector.routes = cn.Spec.AppConnector.Routes.Stringify()
}
}
a.mu.Lock()
if sts.Connector.isExitNode {
if cn.Spec.ExitNode {
a.exitNodes.Add(cn.UID)
} else {
a.exitNodes.Remove(cn.UID)
}
if sts.Connector.routes != "" {
if cn.Spec.SubnetRouter != nil {
a.subnetRouters.Add(cn.GetUID())
} else {
a.subnetRouters.Remove(cn.GetUID())
}
if cn.Spec.AppConnector != nil {
a.appConnectors.Add(cn.GetUID())
} else {
a.appConnectors.Remove(cn.GetUID())
}
a.mu.Unlock()
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
gaugeConnectorAppConnectorResources.Set(int64(a.appConnectors.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
connectors.AddSlice(a.appConnectors.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
_, err := a.ssr.Provision(ctx, logger, sts)
@@ -248,12 +268,15 @@ func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger
a.mu.Lock()
a.subnetRouters.Remove(cn.UID)
a.exitNodes.Remove(cn.UID)
a.appConnectors.Remove(cn.UID)
a.mu.Unlock()
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorAppConnectorResources.Set(int64(a.appConnectors.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
connectors.AddSlice(a.appConnectors.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
return true, nil
}
@@ -262,8 +285,14 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
// Connector fields are already validated at apply time with CEL validation
// on custom resource fields. The checks here are a backup in case the
// CEL validation breaks without us noticing.
if !(cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) {
return errors.New("invalid spec: a Connector must expose subnet routes or act as an exit node (or both)")
if cn.Spec.SubnetRouter == nil && !cn.Spec.ExitNode && cn.Spec.AppConnector == nil {
return errors.New("invalid spec: a Connector must be configured as at least one of subnet router, exit node or app connector")
}
if (cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) && cn.Spec.AppConnector != nil {
return errors.New("invalid spec: a Connector that is configured as an app connector must not be also configured as a subnet router or exit node")
}
if cn.Spec.AppConnector != nil {
return validateAppConnector(cn.Spec.AppConnector)
}
if cn.Spec.SubnetRouter == nil {
return nil
@@ -272,19 +301,27 @@ func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
}
func validateSubnetRouter(sb *tsapi.SubnetRouter) error {
if len(sb.AdvertiseRoutes) < 1 {
if len(sb.AdvertiseRoutes) == 0 {
return errors.New("invalid subnet router spec: no routes defined")
}
var err error
for _, route := range sb.AdvertiseRoutes {
return validateRoutes(sb.AdvertiseRoutes)
}
func validateAppConnector(ac *tsapi.AppConnector) error {
return validateRoutes(ac.Routes)
}
func validateRoutes(routes tsapi.Routes) error {
var errs []error
for _, route := range routes {
pfx, e := netip.ParsePrefix(string(route))
if e != nil {
err = errors.Wrap(err, fmt.Sprintf("route %s is invalid: %v", route, err))
errs = append(errs, fmt.Errorf("route %v is invalid: %v", route, e))
continue
}
if pfx.Masked() != pfx {
err = errors.Wrap(err, fmt.Sprintf("route %s has non-address bits set; expected %s", pfx, pfx.Masked()))
errs = append(errs, fmt.Errorf("route %s has non-address bits set; expected %s", pfx, pfx.Masked()))
}
}
return err
return errors.Join(errs...)
}

View File

@@ -8,12 +8,14 @@ package main
import (
"context"
"testing"
"time"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
@@ -296,3 +298,100 @@ func TestConnectorWithProxyClass(t *testing.T) {
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
}
func TestConnectorWithAppConnector(t *testing.T) {
// Setup
cn := &tsapi.Connector{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
UID: types.UID("1234-UID"),
},
TypeMeta: metav1.TypeMeta{
Kind: tsapi.ConnectorKind,
APIVersion: "tailscale.io/v1alpha1",
},
Spec: tsapi.ConnectorSpec{
AppConnector: &tsapi.AppConnector{},
},
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(cn).
WithStatusSubresource(cn).
Build()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
cl := tstest.NewClock(tstest.ClockOpts{})
fr := record.NewFakeRecorder(1)
cr := &ConnectorReconciler{
Client: fc,
clock: cl,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
recorder: fr,
}
// 1. Connector with app connnector is created and becomes ready
expectReconciled(t, cr, "", "test")
fullName, shortName := findGenName(t, fc, "", "test", "connector")
opts := configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
app: kubetypes.AppConnector,
isAppConnector: true,
}
expectEqual(t, fc, expectedSecret(t, fc, opts), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Connector's ready condition should be set to true
cn.ObjectMeta.Finalizers = append(cn.ObjectMeta.Finalizers, "tailscale.com/finalizer")
cn.Status.IsAppConnector = true
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorCreated,
Message: reasonConnectorCreated,
}}
expectEqual(t, fc, cn, nil)
// 2. Connector with invalid app connector routes has status set to invalid
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("1.2.3.4/5")}
})
cn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("1.2.3.4/5")}
expectReconciled(t, cr, "", "test")
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorInvalid,
Message: "Connector is invalid: route 1.2.3.4/5 has non-address bits set; expected 0.0.0.0/5",
}}
expectEqual(t, fc, cn, nil)
// 3. Connector with valid app connnector routes becomes ready
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("10.88.2.21/32")}
})
cn.Spec.AppConnector.Routes = tsapi.Routes{tsapi.Route("10.88.2.21/32")}
cn.Status.Conditions = []metav1.Condition{{
Type: string(tsapi.ConnectorReady),
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
Reason: reasonConnectorCreated,
Message: reasonConnectorCreated,
}}
expectReconciled(t, cr, "", "test")
}

View File

@@ -80,10 +80,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -658,6 +654,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
@@ -740,7 +737,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -775,6 +771,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/appc+
@@ -792,7 +789,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/appc+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
@@ -812,8 +809,11 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/util/slicesx from tailscale.com/appc+
tailscale.com/util/syspolicy from tailscale.com/control/controlclient+
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/control/controlclient+
@@ -823,7 +823,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/util/vizerror from tailscale.com/tailcfg+
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+

View File

@@ -24,6 +24,10 @@ spec:
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Whether this Connector instance is an app connector.
jsonPath: .status.isAppConnector
name: IsAppConnector
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
@@ -66,10 +70,40 @@ spec:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
type: object
properties:
appConnector:
description: |-
AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
Connector does not act as an app connector.
Note that you will need to manually configure the permissions and the domains for the app connector via the
Admin panel.
Note also that the main tested and supported use case of this config option is to deploy an app connector on
Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
tested or optimised for.
If you are using the app connector to access SaaS applications because you need a predictable egress IP that
can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
device with a static IP address.
https://tailscale.com/kb/1281/app-connectors
type: object
properties:
routes:
description: |-
Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
type: array
minItems: 1
items:
type: string
format: cidr
exitNode:
description: |-
ExitNode defines whether the Connector node should act as a
Tailscale exit node. Defaults to false.
ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
This field is mutually exclusive with the appConnector field.
https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
@@ -90,9 +124,11 @@ spec:
type: string
subnetRouter:
description: |-
SubnetRouter defines subnet routes that the Connector node should
expose to tailnet. If unset, none are exposed.
SubnetRouter defines subnet routes that the Connector device should
expose to tailnet as a Tailscale subnet router.
https://tailscale.com/kb/1019/subnets/
If this field is unset, the device does not get configured as a Tailscale subnet router.
This field is mutually exclusive with the appConnector field.
type: object
required:
- advertiseRoutes
@@ -125,8 +161,10 @@ spec:
type: string
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
x-kubernetes-validations:
- rule: has(self.subnetRouter) || self.exitNode == true
message: A Connector needs to be either an exit node or a subnet router, or both.
- rule: has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)
message: A Connector needs to have at least one of exit node, subnet router or app connector configured.
- rule: '!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))'
message: The appConnector field is mutually exclusive with exitNode and subnetRouter fields.
status:
description: |-
ConnectorStatus describes the status of the Connector. This is set
@@ -200,6 +238,9 @@ spec:
If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the
node.
type: string
isAppConnector:
description: IsAppConnector is set to true if the Connector acts as an app connector.
type: boolean
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean

View File

@@ -1896,6 +1896,182 @@ spec:
Value is the taint value the toleration matches to.
If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
topologySpreadConstraints:
description: |-
Proxy Pod's topology spread constraints.
By default Tailscale Kubernetes operator does not apply any topology spread constraints.
https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
type: array
items:
description: TopologySpreadConstraint specifies how to spread matching pods among the given topology.
type: object
required:
- maxSkew
- topologyKey
- whenUnsatisfiable
properties:
labelSelector:
description: |-
LabelSelector is used to find matching pods.
Pods that match this label selector are counted to determine the number of pods
in their corresponding topology domain.
type: object
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
type: array
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
type: object
required:
- key
- operator
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
type: array
items:
type: string
x-kubernetes-list-type: atomic
x-kubernetes-list-type: atomic
matchLabels:
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
additionalProperties:
type: string
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select the pods over which
spreading will be calculated. The keys are used to lookup values from the
incoming pod labels, those key-value labels are ANDed with labelSelector
to select the group of existing pods over which spreading will be calculated
for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector.
MatchLabelKeys cannot be set when LabelSelector isn't set.
Keys that don't exist in the incoming pod labels will
be ignored. A null or empty list means only match against labelSelector.
This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default).
type: array
items:
type: string
x-kubernetes-list-type: atomic
maxSkew:
description: |-
MaxSkew describes the degree to which pods may be unevenly distributed.
When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference
between the number of matching pods in the target topology and the global minimum.
The global minimum is the minimum number of matching pods in an eligible domain
or zero if the number of eligible domains is less than MinDomains.
For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same
labelSelector spread as 2/2/1:
In this case, the global minimum is 1.
| zone1 | zone2 | zone3 |
| P P | P P | P |
- if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2;
scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2)
violate MaxSkew(1).
- if MaxSkew is 2, incoming pod can be scheduled onto any zone.
When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence
to topologies that satisfy it.
It's a required field. Default value is 1 and 0 is not allowed.
type: integer
format: int32
minDomains:
description: |-
MinDomains indicates a minimum number of eligible domains.
When the number of eligible domains with matching topology keys is less than minDomains,
Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed.
And when the number of eligible domains with matching topology keys equals or greater than minDomains,
this value has no effect on scheduling.
As a result, when the number of eligible domains is less than minDomains,
scheduler won't schedule more than maxSkew Pods to those domains.
If value is nil, the constraint behaves as if MinDomains is equal to 1.
Valid values are integers greater than 0.
When value is not nil, WhenUnsatisfiable must be DoNotSchedule.
For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same
labelSelector spread as 2/2/2:
| zone1 | zone2 | zone3 |
| P P | P P | P P |
The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0.
In this situation, new pod with the same labelSelector cannot be scheduled,
because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones,
it will violate MaxSkew.
type: integer
format: int32
nodeAffinityPolicy:
description: |-
NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector
when calculating pod topology spread skew. Options are:
- Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations.
- Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations.
If this value is nil, the behavior is equivalent to the Honor policy.
This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.
type: string
nodeTaintsPolicy:
description: |-
NodeTaintsPolicy indicates how we will treat node taints when calculating
pod topology spread skew. Options are:
- Honor: nodes without taints, along with tainted nodes for which the incoming pod
has a toleration, are included.
- Ignore: node taints are ignored. All nodes are included.
If this value is nil, the behavior is equivalent to the Ignore policy.
This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.
type: string
topologyKey:
description: |-
TopologyKey is the key of node labels. Nodes that have a label with this key
and identical values are considered to be in the same topology.
We consider each <key, value> as a "bucket", and try to put balanced number
of pods into each bucket.
We define a domain as a particular instance of a topology.
Also, we define an eligible domain as a domain whose nodes meet the requirements of
nodeAffinityPolicy and nodeTaintsPolicy.
e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology.
And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology.
It's a required field.
type: string
whenUnsatisfiable:
description: |-
WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy
the spread constraint.
- DoNotSchedule (default) tells the scheduler not to schedule it.
- ScheduleAnyway tells the scheduler to schedule the pod in any location,
but giving higher precedence to topologies that would help reduce the
skew.
A constraint is considered "Unsatisfiable" for an incoming pod
if and only if every possible node assignment for that pod would violate
"MaxSkew" on some topology.
For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same
labelSelector spread as 3/1/1:
| zone1 | zone2 | zone3 |
| P P P | P | P |
If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled
to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies
MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler
won't make it *more* imbalanced.
It's a required field.
type: string
tailscale:
description: |-
TailscaleConfig contains options to configure the tailscale-specific

View File

@@ -53,6 +53,10 @@ spec:
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Whether this Connector instance is an app connector.
jsonPath: .status.isAppConnector
name: IsAppConnector
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
@@ -91,10 +95,40 @@ spec:
More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
appConnector:
description: |-
AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
Connector does not act as an app connector.
Note that you will need to manually configure the permissions and the domains for the app connector via the
Admin panel.
Note also that the main tested and supported use case of this config option is to deploy an app connector on
Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
tested or optimised for.
If you are using the app connector to access SaaS applications because you need a predictable egress IP that
can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
device with a static IP address.
https://tailscale.com/kb/1281/app-connectors
properties:
routes:
description: |-
Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
items:
format: cidr
type: string
minItems: 1
type: array
type: object
exitNode:
description: |-
ExitNode defines whether the Connector node should act as a
Tailscale exit node. Defaults to false.
ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
This field is mutually exclusive with the appConnector field.
https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
@@ -115,9 +149,11 @@ spec:
type: string
subnetRouter:
description: |-
SubnetRouter defines subnet routes that the Connector node should
expose to tailnet. If unset, none are exposed.
SubnetRouter defines subnet routes that the Connector device should
expose to tailnet as a Tailscale subnet router.
https://tailscale.com/kb/1019/subnets/
If this field is unset, the device does not get configured as a Tailscale subnet router.
This field is mutually exclusive with the appConnector field.
properties:
advertiseRoutes:
description: |-
@@ -151,8 +187,10 @@ spec:
type: array
type: object
x-kubernetes-validations:
- message: A Connector needs to be either an exit node or a subnet router, or both.
rule: has(self.subnetRouter) || self.exitNode == true
- message: A Connector needs to have at least one of exit node, subnet router or app connector configured.
rule: has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)
- message: The appConnector field is mutually exclusive with exitNode and subnetRouter fields.
rule: '!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))'
status:
description: |-
ConnectorStatus describes the status of the Connector. This is set
@@ -225,6 +263,9 @@ spec:
If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the
node.
type: string
isAppConnector:
description: IsAppConnector is set to true if the Connector acts as an app connector.
type: boolean
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean
@@ -2323,6 +2364,182 @@ spec:
type: string
type: object
type: array
topologySpreadConstraints:
description: |-
Proxy Pod's topology spread constraints.
By default Tailscale Kubernetes operator does not apply any topology spread constraints.
https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
items:
description: TopologySpreadConstraint specifies how to spread matching pods among the given topology.
properties:
labelSelector:
description: |-
LabelSelector is used to find matching pods.
Pods that match this label selector are counted to determine the number of pods
in their corresponding topology domain.
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
matchLabelKeys:
description: |-
MatchLabelKeys is a set of pod label keys to select the pods over which
spreading will be calculated. The keys are used to lookup values from the
incoming pod labels, those key-value labels are ANDed with labelSelector
to select the group of existing pods over which spreading will be calculated
for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector.
MatchLabelKeys cannot be set when LabelSelector isn't set.
Keys that don't exist in the incoming pod labels will
be ignored. A null or empty list means only match against labelSelector.
This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default).
items:
type: string
type: array
x-kubernetes-list-type: atomic
maxSkew:
description: |-
MaxSkew describes the degree to which pods may be unevenly distributed.
When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference
between the number of matching pods in the target topology and the global minimum.
The global minimum is the minimum number of matching pods in an eligible domain
or zero if the number of eligible domains is less than MinDomains.
For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same
labelSelector spread as 2/2/1:
In this case, the global minimum is 1.
| zone1 | zone2 | zone3 |
| P P | P P | P |
- if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2;
scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2)
violate MaxSkew(1).
- if MaxSkew is 2, incoming pod can be scheduled onto any zone.
When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence
to topologies that satisfy it.
It's a required field. Default value is 1 and 0 is not allowed.
format: int32
type: integer
minDomains:
description: |-
MinDomains indicates a minimum number of eligible domains.
When the number of eligible domains with matching topology keys is less than minDomains,
Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed.
And when the number of eligible domains with matching topology keys equals or greater than minDomains,
this value has no effect on scheduling.
As a result, when the number of eligible domains is less than minDomains,
scheduler won't schedule more than maxSkew Pods to those domains.
If value is nil, the constraint behaves as if MinDomains is equal to 1.
Valid values are integers greater than 0.
When value is not nil, WhenUnsatisfiable must be DoNotSchedule.
For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same
labelSelector spread as 2/2/2:
| zone1 | zone2 | zone3 |
| P P | P P | P P |
The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0.
In this situation, new pod with the same labelSelector cannot be scheduled,
because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones,
it will violate MaxSkew.
format: int32
type: integer
nodeAffinityPolicy:
description: |-
NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector
when calculating pod topology spread skew. Options are:
- Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations.
- Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations.
If this value is nil, the behavior is equivalent to the Honor policy.
This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.
type: string
nodeTaintsPolicy:
description: |-
NodeTaintsPolicy indicates how we will treat node taints when calculating
pod topology spread skew. Options are:
- Honor: nodes without taints, along with tainted nodes for which the incoming pod
has a toleration, are included.
- Ignore: node taints are ignored. All nodes are included.
If this value is nil, the behavior is equivalent to the Ignore policy.
This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag.
type: string
topologyKey:
description: |-
TopologyKey is the key of node labels. Nodes that have a label with this key
and identical values are considered to be in the same topology.
We consider each <key, value> as a "bucket", and try to put balanced number
of pods into each bucket.
We define a domain as a particular instance of a topology.
Also, we define an eligible domain as a domain whose nodes meet the requirements of
nodeAffinityPolicy and nodeTaintsPolicy.
e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology.
And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology.
It's a required field.
type: string
whenUnsatisfiable:
description: |-
WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy
the spread constraint.
- DoNotSchedule (default) tells the scheduler not to schedule it.
- ScheduleAnyway tells the scheduler to schedule the pod in any location,
but giving higher precedence to topologies that would help reduce the
skew.
A constraint is considered "Unsatisfiable" for an incoming pod
if and only if every possible node assignment for that pod would violate
"MaxSkew" on some topology.
For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same
labelSelector spread as 3/1/1:
| zone1 | zone2 | zone3 |
| P P P | P | P |
If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled
to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies
MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler
won't make it *more* imbalanced.
It's a required field.
type: string
required:
- maxSkew
- topologyKey
- whenUnsatisfiable
type: object
type: array
type: object
type: object
tailscale:

View File

@@ -11,6 +11,7 @@ import (
"context"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -150,6 +151,13 @@ func initTSNet(zlog *zap.SugaredLogger) (*tsnet.Server, *tailscale.Client) {
Hostname: hostname,
Logf: zlog.Named("tailscaled").Debugf,
}
if p := os.Getenv("TS_PORT"); p != "" {
port, err := strconv.ParseUint(p, 10, 16)
if err != nil {
startlog.Fatalf("TS_PORT %q cannot be parsed as uint16: %v", p, err)
}
s.Port = uint16(port)
}
if kubeSecret != "" {
st, err := kubestore.New(logger.Discard, kubeSecret)
if err != nil {

View File

@@ -432,6 +432,148 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
}
func TestTailnetTargetIPAnnotation_IPCouldNotBeParsed(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
clock := tstest.NewClock(tstest.ClockOpts{})
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
clock: clock,
recorder: record.NewFakeRecorder(100),
}
tailnetTargetIP := "invalid-ip"
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
Annotations: map[string]string{
AnnotationTailnetTargetIP: tailnetTargetIP,
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
})
expectReconciled(t, sr, "default", "test")
t0 := conditionTime(clock)
want := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
Annotations: map[string]string{
AnnotationTailnetTargetIP: tailnetTargetIP,
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
Status: corev1.ServiceStatus{
Conditions: []metav1.Condition{{
Type: string(tsapi.ProxyReady),
Status: metav1.ConditionFalse,
LastTransitionTime: t0,
Reason: reasonProxyInvalid,
Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-ip: "invalid-ip" could not be parsed as a valid IP Address, error: ParseAddr("invalid-ip"): unable to parse IP`,
}},
},
}
expectEqual(t, fc, want, nil)
}
func TestTailnetTargetIPAnnotation_InvalidIP(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
clock := tstest.NewClock(tstest.ClockOpts{})
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
clock: clock,
recorder: record.NewFakeRecorder(100),
}
tailnetTargetIP := "999.999.999.999"
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
Annotations: map[string]string{
AnnotationTailnetTargetIP: tailnetTargetIP,
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
})
expectReconciled(t, sr, "default", "test")
t0 := conditionTime(clock)
want := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
Annotations: map[string]string{
AnnotationTailnetTargetIP: tailnetTargetIP,
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
Status: corev1.ServiceStatus{
Conditions: []metav1.Condition{{
Type: string(tsapi.ProxyReady),
Status: metav1.ConditionFalse,
LastTransitionTime: t0,
Reason: reasonProxyInvalid,
Message: `unable to provision proxy resources: invalid Service: invalid value of annotation tailscale.com/tailnet-ip: "999.999.999.999" could not be parsed as a valid IP Address, error: ParseAddr("999.999.999.999"): IPv4 field has value >255`,
}},
},
}
expectEqual(t, fc, want, nil)
}
func TestAnnotations(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
@@ -1246,7 +1388,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "e09bededa0379920141cbd0b0dbdf9b8b66545877f9e8397423f5ce3e1ba439e",
confFileHash: "362360188dac62bca8013c8134929fed8efd84b1f410c00873d14a05709b5647",
app: kubetypes.AppIngressProxy,
}
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
@@ -1257,7 +1399,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
mak.Set(&svc.Annotations, AnnotationHostname, "another-test")
})
o.hostname = "another-test"
o.confFileHash = "5d754cf55463135ee34aa9821f2fd8483b53eb0570c3740c84a086304f427684"
o.confFileHash = "20db57cfabc3fc6490f6bb1dc85994e61d255cdfa2a56abb0141736e59f263ef"
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
}

View File

@@ -47,7 +47,7 @@ const (
reasonProxyGroupInvalid = "ProxyGroupInvalid"
)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupCount)
var gaugeProxyGroupResources = clientmetric.NewGauge(kubetypes.MetricProxyGroupEgressCount)
// ProxyGroupReconciler ensures cluster resources for a ProxyGroup definition.
type ProxyGroupReconciler struct {
@@ -353,7 +353,7 @@ func (r *ProxyGroupReconciler) deleteTailnetDevice(ctx context.Context, id tailc
func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, pg *tsapi.ProxyGroup, proxyClass *tsapi.ProxyClass) (hash string, err error) {
logger := r.logger(pg.Name)
var allConfigs []tailscaledConfigs
var configSHA256Sum string
for i := range pgReplicas(pg) {
cfgSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
@@ -389,7 +389,6 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
if err != nil {
return "", fmt.Errorf("error creating tailscaled config: %w", err)
}
allConfigs = append(allConfigs, configs)
for cap, cfg := range configs {
cfgJSON, err := json.Marshal(cfg)
@@ -399,6 +398,32 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
mak.Set(&cfgSecret.StringData, tsoperator.TailscaledConfigFileName(cap), string(cfgJSON))
}
// The config sha256 sum is a value for a hash annotation used to trigger
// pod restarts when tailscaled config changes. Any config changes apply
// to all replicas, so it is sufficient to only hash the config for the
// first replica.
//
// In future, we're aiming to eliminate restarts altogether and have
// pods dynamically reload their config when it changes.
if i == 0 {
sum := sha256.New()
for _, cfg := range configs {
// Zero out the auth key so it doesn't affect the sha256 hash when we
// remove it from the config after the pods have all authed. Otherwise
// all the pods will need to restart immediately after authing.
cfg.AuthKey = nil
b, err := json.Marshal(cfg)
if err != nil {
return "", err
}
if _, err := sum.Write(b); err != nil {
return "", err
}
}
configSHA256Sum = fmt.Sprintf("%x", sum.Sum(nil))
}
if existingCfgSecret != nil {
logger.Debugf("patching the existing ProxyGroup config Secret %s", cfgSecret.Name)
if err := r.Patch(ctx, cfgSecret, client.MergeFrom(existingCfgSecret)); err != nil {
@@ -412,16 +437,7 @@ func (r *ProxyGroupReconciler) ensureConfigSecretsCreated(ctx context.Context, p
}
}
sum := sha256.New()
b, err := json.Marshal(allConfigs)
if err != nil {
return "", err
}
if _, err := sum.Write(b); err != nil {
return "", err
}
return fmt.Sprintf("%x", sum.Sum(nil)), nil
return configSHA256Sum, nil
}
func pgTailscaledConfig(pg *tsapi.ProxyGroup, class *tsapi.ProxyClass, idx int32, authKey string, oldSecret *corev1.Secret) (tailscaledConfigs, error) {

View File

@@ -15,6 +15,7 @@ import (
"sigs.k8s.io/yaml"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/ptr"
)
@@ -92,6 +93,10 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode, cfgHa
c.Image = image
c.VolumeMounts = func() []corev1.VolumeMount {
var mounts []corev1.VolumeMount
// TODO(tomhjp): Read config directly from the secret instead. The
// mounts change on scaling up/down which causes unnecessary restarts
// for pods that haven't meaningfully changed.
for i := range pgReplicas(pg) {
mounts = append(mounts, corev1.VolumeMount{
Name: fmt.Sprintf("tailscaledconfig-%d", i),
@@ -146,6 +151,10 @@ func pgStatefulSet(pg *tsapi.ProxyGroup, namespace, image, tsFirewallMode, cfgHa
Name: "TS_USERSPACE",
Value: "false",
},
{
Name: "TS_INTERNAL_APP",
Value: kubetypes.AppProxyGroupEgress,
},
}
if tsFirewallMode != "" {

View File

@@ -35,6 +35,8 @@ var defaultProxyClassAnnotations = map[string]string{
}
func TestProxyGroup(t *testing.T) {
const initialCfgHash = "6632726be70cf224049580deb4d317bba065915b5fd415461d60ed621c91b196"
pc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: "default-pc",
@@ -80,6 +82,7 @@ func TestProxyGroup(t *testing.T) {
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "the ProxyGroup's ProxyClass default-pc is not yet in a ready state, waiting...", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, false, "")
})
t.Run("observe_ProxyGroupCreating_status_reason", func(t *testing.T) {
@@ -100,10 +103,11 @@ func TestProxyGroup(t *testing.T) {
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "0/2 ProxyGroup pods running", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
if expected := 1; reconciler.proxyGroups.Len() != expected {
t.Fatalf("expected %d recorders, got %d", expected, reconciler.proxyGroups.Len())
}
expectProxyGroupResources(t, fc, pg, true)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
keyReq := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
@@ -135,7 +139,7 @@ func TestProxyGroup(t *testing.T) {
}
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionTrue, reasonProxyGroupReady, reasonProxyGroupReady, 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
})
t.Run("scale_up_to_3", func(t *testing.T) {
@@ -146,6 +150,7 @@ func TestProxyGroup(t *testing.T) {
expectReconciled(t, reconciler, "", pg.Name)
tsoperator.SetProxyGroupCondition(pg, tsapi.ProxyGroupReady, metav1.ConditionFalse, reasonProxyGroupCreating, "2/3 ProxyGroup pods running", 0, cl, zl.Sugar())
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
addNodeIDToStateSecrets(t, fc, pg)
expectReconciled(t, reconciler, "", pg.Name)
@@ -155,7 +160,7 @@ func TestProxyGroup(t *testing.T) {
TailnetIPs: []string{"1.2.3.4", "::1"},
})
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
})
t.Run("scale_down_to_1", func(t *testing.T) {
@@ -163,11 +168,26 @@ func TestProxyGroup(t *testing.T) {
mustUpdate(t, fc, "", pg.Name, func(p *tsapi.ProxyGroup) {
p.Spec = pg.Spec
})
expectReconciled(t, reconciler, "", pg.Name)
pg.Status.Devices = pg.Status.Devices[:1] // truncate to only the first device.
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, initialCfgHash)
})
expectProxyGroupResources(t, fc, pg, true)
t.Run("trigger_config_change_and_observe_new_config_hash", func(t *testing.T) {
pc.Spec.TailscaleConfig = &tsapi.TailscaleConfig{
AcceptRoutes: true,
}
mustUpdate(t, fc, "", pc.Name, func(p *tsapi.ProxyClass) {
p.Spec = pc.Spec
})
expectReconciled(t, reconciler, "", pg.Name)
expectEqual(t, fc, pg, nil)
expectProxyGroupResources(t, fc, pg, true, "518a86e9fae64f270f8e0ec2a2ea6ca06c10f725035d3d6caca132cd61e42a74")
})
t.Run("delete_and_cleanup", func(t *testing.T) {
@@ -191,13 +211,13 @@ func TestProxyGroup(t *testing.T) {
})
}
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool) {
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool, cfgHash string) {
t.Helper()
role := pgRole(pg, tsNamespace)
roleBinding := pgRoleBinding(pg, tsNamespace)
serviceAccount := pgServiceAccount(pg, tsNamespace)
statefulSet, err := pgStatefulSet(pg, tsNamespace, testProxyImage, "auto", "")
statefulSet, err := pgStatefulSet(pg, tsNamespace, testProxyImage, "auto", cfgHash)
if err != nil {
t.Fatal(err)
}
@@ -207,9 +227,7 @@ func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.Prox
expectEqual(t, fc, role, nil)
expectEqual(t, fc, roleBinding, nil)
expectEqual(t, fc, serviceAccount, nil)
expectEqual(t, fc, statefulSet, func(ss *appsv1.StatefulSet) {
ss.Spec.Template.Annotations[podAnnotationLastSetConfigFileHash] = ""
})
expectEqual(t, fc, statefulSet, nil)
} else {
expectMissing[rbacv1.Role](t, fc, role.Namespace, role.Name)
expectMissing[rbacv1.RoleBinding](t, fc, roleBinding.Namespace, roleBinding.Name)
@@ -218,11 +236,13 @@ func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.Prox
}
var expectedSecrets []string
for i := range pgReplicas(pg) {
expectedSecrets = append(expectedSecrets,
fmt.Sprintf("%s-%d", pg.Name, i),
fmt.Sprintf("%s-%d-config", pg.Name, i),
)
if shouldExist {
for i := range pgReplicas(pg) {
expectedSecrets = append(expectedSecrets,
fmt.Sprintf("%s-%d", pg.Name, i),
fmt.Sprintf("%s-%d-config", pg.Name, i),
)
}
}
expectSecrets(t, fc, expectedSecrets)
}

View File

@@ -132,10 +132,13 @@ type tailscaleSTSConfig struct {
}
type connector struct {
// routes is a list of subnet routes that this Connector should expose.
// routes is a list of routes that this Connector should advertise either as a subnet router or as an app
// connector.
routes string
// isExitNode defines whether this Connector should act as an exit node.
isExitNode bool
// isAppConnector defines whether this Connector should act as an app connector.
isAppConnector bool
}
type tsnetServer interface {
CertDomains() []string
@@ -674,7 +677,7 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
}
if stsCfg != nil && pc.Spec.Metrics != nil && pc.Spec.Metrics.Enable {
if stsCfg.TailnetTargetFQDN == "" && stsCfg.TailnetTargetIP == "" && !stsCfg.ForwardClusterTrafficViaL7IngressProxy {
enableMetrics(ss, pc)
enableMetrics(ss)
} else if stsCfg.ForwardClusterTrafficViaL7IngressProxy {
// TODO (irbekrm): fix this
// For Ingress proxies that have been configured with
@@ -718,6 +721,7 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
ss.Spec.Template.Spec.NodeSelector = wantsPod.NodeSelector
ss.Spec.Template.Spec.Affinity = wantsPod.Affinity
ss.Spec.Template.Spec.Tolerations = wantsPod.Tolerations
ss.Spec.Template.Spec.TopologySpreadConstraints = wantsPod.TopologySpreadConstraints
// Update containers.
updateContainer := func(overlay *tsapi.Container, base corev1.Container) corev1.Container {
@@ -762,7 +766,7 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
return ss
}
func enableMetrics(ss *appsv1.StatefulSet, pc *tsapi.ProxyClass) {
func enableMetrics(ss *appsv1.StatefulSet) {
for i, c := range ss.Spec.Template.Spec.Containers {
if c.Name == "tailscale" {
// Serve metrics on on <pod-ip>:9001/debug/metrics. If
@@ -802,11 +806,13 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
Locked: "false",
Hostname: &stsC.Hostname,
NoStatefulFiltering: "false",
AppConnector: &ipn.AppConnectorPrefs{Advertise: false},
}
// For egress proxies only, we need to ensure that stateful filtering is
// not in place so that traffic from cluster can be forwarded via
// Tailscale IPs.
// TODO (irbekrm): set it to true always as this is now the default in core.
if stsC.TailnetTargetFQDN != "" || stsC.TailnetTargetIP != "" {
conf.NoStatefulFiltering = "true"
}
@@ -816,6 +822,9 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
return nil, fmt.Errorf("error calculating routes: %w", err)
}
conf.AdvertiseRoutes = routes
if stsC.Connector.isAppConnector {
conf.AppConnector.Advertise = true
}
}
if shouldAcceptRoutes(stsC.ProxyClass) {
conf.AcceptRoutes = "true"
@@ -830,9 +839,15 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
}
conf.AuthKey = key
}
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
capVerConfigs[107] = *conf
// AppConnector config option is only understood by clients of capver 107 and newer.
conf.AppConnector = nil
capVerConfigs[95] = *conf
// legacy config should not contain NoStatefulFiltering field.
// StatefulFiltering is only understood by clients of capver 95 and newer.
conf.NoStatefulFiltering.Clear()
capVerConfigs[94] = *conf
return capVerConfigs, nil

View File

@@ -18,6 +18,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/types/ptr"
@@ -73,6 +74,16 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
NodeSelector: map[string]string{"beta.kubernetes.io/os": "linux"},
Affinity: &corev1.Affinity{NodeAffinity: &corev1.NodeAffinity{RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{}}},
Tolerations: []corev1.Toleration{{Key: "", Operator: "Exists"}},
TopologySpreadConstraints: []corev1.TopologySpreadConstraint{
{
WhenUnsatisfiable: "DoNotSchedule",
TopologyKey: "kubernetes.io/hostname",
MaxSkew: 3,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
},
},
TailscaleContainer: &tsapi.Container{
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
@@ -159,6 +170,7 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
wantSS.Spec.Template.Spec.NodeSelector = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeSelector
wantSS.Spec.Template.Spec.Affinity = proxyClassAllOpts.Spec.StatefulSet.Pod.Affinity
wantSS.Spec.Template.Spec.Tolerations = proxyClassAllOpts.Spec.StatefulSet.Pod.Tolerations
wantSS.Spec.Template.Spec.TopologySpreadConstraints = proxyClassAllOpts.Spec.StatefulSet.Pod.TopologySpreadConstraints
wantSS.Spec.Template.Spec.Containers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.SecurityContext
wantSS.Spec.Template.Spec.InitContainers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleInitContainer.SecurityContext
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
@@ -201,6 +213,7 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
wantSS.Spec.Template.Spec.NodeSelector = proxyClassAllOpts.Spec.StatefulSet.Pod.NodeSelector
wantSS.Spec.Template.Spec.Affinity = proxyClassAllOpts.Spec.StatefulSet.Pod.Affinity
wantSS.Spec.Template.Spec.Tolerations = proxyClassAllOpts.Spec.StatefulSet.Pod.Tolerations
wantSS.Spec.Template.Spec.TopologySpreadConstraints = proxyClassAllOpts.Spec.StatefulSet.Pod.TopologySpreadConstraints
wantSS.Spec.Template.Spec.Containers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.SecurityContext
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
wantSS.Spec.Template.Spec.Containers[0].Env = append(wantSS.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}}...)

View File

@@ -358,9 +358,14 @@ func validateService(svc *corev1.Service) []string {
violations = append(violations, fmt.Sprintf("invalid value of annotation %s: %q does not appear to be a valid MagicDNS name", AnnotationTailnetTargetFQDN, fqdn))
}
}
// TODO(irbekrm): validate that tailscale.com/tailnet-ip annotation is a
// valid IP address (tailscale/tailscale#13671).
if ipStr := svc.Annotations[AnnotationTailnetTargetIP]; ipStr != "" {
ip, err := netip.ParseAddr(ipStr)
if err != nil {
violations = append(violations, fmt.Sprintf("invalid value of annotation %s: %q could not be parsed as a valid IP Address, error: %s", AnnotationTailnetTargetIP, ipStr, err))
} else if !ip.IsValid() {
violations = append(violations, fmt.Sprintf("parsed IP address in annotation %s: %q is not valid", AnnotationTailnetTargetIP, ipStr))
}
}
svcName := nameForService(svc)
if err := dnsname.ValidLabel(svcName); err != nil {

View File

@@ -48,6 +48,7 @@ type configOpts struct {
clusterTargetDNS string
subnetRoutes string
isExitNode bool
isAppConnector bool
confFileHash string
serveConfig *ipn.ServeConfig
shouldEnableForwardingClusterTrafficViaIngress bool
@@ -356,6 +357,7 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
AcceptRoutes: "false",
AppConnector: &ipn.AppConnectorPrefs{Advertise: false},
}
if opts.proxyClass != "" {
t.Logf("applying configuration from ProxyClass %s", opts.proxyClass)
@@ -370,6 +372,9 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
if opts.shouldRemoveAuthKey {
conf.AuthKey = nil
}
if opts.isAppConnector {
conf.AppConnector = &ipn.AppConnectorPrefs{Advertise: true}
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {
r := opts.subnetRoutes
@@ -384,22 +389,29 @@ func expectedSecret(t *testing.T, cl client.Client, opts configOpts) *corev1.Sec
routes = append(routes, prefix)
}
}
conf.AdvertiseRoutes = routes
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
if opts.tailnetTargetFQDN != "" || opts.tailnetTargetIP != "" {
conf.NoStatefulFiltering = "true"
} else {
conf.NoStatefulFiltering = "false"
}
conf.AdvertiseRoutes = routes
bnn, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
conf.AppConnector = nil
bn, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
conf.NoStatefulFiltering.Clear()
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
mak.Set(&s.StringData, "tailscaled", string(b))
mak.Set(&s.StringData, "cap-95.hujson", string(bn))
mak.Set(&s.StringData, "cap-107.hujson", string(bnn))
labels := map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
@@ -674,5 +686,17 @@ func removeAuthKeyIfExistsModifier(t *testing.T) func(s *corev1.Secret) {
}
mak.Set(&secret.StringData, "cap-95.hujson", string(b))
}
if len(secret.StringData["cap-107.hujson"]) != 0 {
conf := &ipn.ConfigVAlpha{}
if err := json.Unmarshal([]byte(secret.StringData["cap-107.hujson"]), conf); err != nil {
t.Fatalf("error umarshalling 'cap-107.hujson' contents: %v", err)
}
conf.AuthKey = nil
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling 'cap-107.huson' contents: %v", err)
}
mak.Set(&secret.StringData, "cap-107.hujson", string(b))
}
}
}

View File

@@ -67,6 +67,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/types/logger from tailscale.com/tsweb
tailscale.com/types/opt from tailscale.com/envknob+
tailscale.com/types/ptr from tailscale.com/tailcfg+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/tailcfg+
tailscale.com/types/tkatype from tailscale.com/tailcfg+
tailscale.com/types/views from tailscale.com/net/tsaddr+
@@ -74,7 +75,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/fastuuid from tailscale.com/tsweb
tailscale.com/util/lineread from tailscale.com/version/distro
tailscale.com/util/lineiter from tailscale.com/version/distro
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/slicesx from tailscale.com/tailcfg
tailscale.com/util/vizerror from tailscale.com/tailcfg+

View File

@@ -185,10 +185,12 @@ change in the future.
logoutCmd,
switchCmd,
configureCmd,
syspolicyCmd,
netcheckCmd,
ipCmd,
dnsCmd,
statusCmd,
metricsCmd,
pingCmd,
ncCmd,
sshCmd,

View File

@@ -0,0 +1,88 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"errors"
"fmt"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/atomicfile"
)
var metricsCmd = &ffcli.Command{
Name: "metrics",
ShortHelp: "Show Tailscale metrics",
LongHelp: strings.TrimSpace(`
The 'tailscale metrics' command shows Tailscale user-facing metrics (as opposed
to internal metrics printed by 'tailscale debug metrics').
For more information about Tailscale metrics, refer to
https://tailscale.com/s/client-metrics
`),
ShortUsage: "tailscale metrics <subcommand> [flags]",
UsageFunc: usageFuncNoDefaultValues,
Exec: runMetricsNoSubcommand,
Subcommands: []*ffcli.Command{
{
Name: "print",
ShortUsage: "tailscale metrics print",
Exec: runMetricsPrint,
ShortHelp: "Prints current metric values in the Prometheus text exposition format",
},
{
Name: "write",
ShortUsage: "tailscale metrics write <path>",
Exec: runMetricsWrite,
ShortHelp: "Writes metric values to a file",
LongHelp: strings.TrimSpace(`
The 'tailscale metrics write' command writes metric values to a text file provided as its
only argument. It's meant to be used alongside Prometheus node exporter, allowing Tailscale
metrics to be consumed and exported by the textfile collector.
As an example, to export Tailscale metrics on an Ubuntu system running node exporter, you
can regularly run 'tailscale metrics write /var/lib/prometheus/node-exporter/tailscaled.prom'
using cron or a systemd timer.
`),
},
},
}
// runMetricsNoSubcommand prints metric values if no subcommand is specified.
func runMetricsNoSubcommand(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("tailscale metrics: unknown subcommand: %s", args[0])
}
return runMetricsPrint(ctx, args)
}
// runMetricsPrint prints metric values to stdout.
func runMetricsPrint(ctx context.Context, args []string) error {
out, err := localClient.UserMetrics(ctx)
if err != nil {
return err
}
Stdout.Write(out)
return nil
}
// runMetricsWrite writes metric values to a file.
func runMetricsWrite(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: tailscale metrics write <path>")
}
path := args[0]
out, err := localClient.UserMetrics(ctx)
if err != nil {
return err
}
return atomicfile.WriteFile(path, out, 0644)
}

View File

@@ -0,0 +1,110 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"slices"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/util/syspolicy/setting"
)
var syspolicyArgs struct {
json bool // JSON output mode
}
var syspolicyCmd = &ffcli.Command{
Name: "syspolicy",
ShortHelp: "Diagnose the MDM and system policy configuration",
LongHelp: "The 'tailscale syspolicy' command provides tools for diagnosing the MDM and system policy configuration.",
ShortUsage: "tailscale syspolicy <subcommand>",
UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{
{
Name: "list",
ShortUsage: "tailscale syspolicy list",
Exec: runSysPolicyList,
ShortHelp: "Prints effective policy settings",
LongHelp: "The 'tailscale syspolicy list' subcommand displays the effective policy settings and their sources (e.g., MDM or environment variables).",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("syspolicy list")
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
return fs
})(),
},
{
Name: "reload",
ShortUsage: "tailscale syspolicy reload",
Exec: runSysPolicyReload,
ShortHelp: "Forces a reload of policy settings, even if no changes are detected, and prints the result",
LongHelp: "The 'tailscale syspolicy reload' subcommand forces a reload of policy settings, even if no changes are detected, and prints the result.",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("syspolicy reload")
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
return fs
})(),
},
},
}
func runSysPolicyList(ctx context.Context, args []string) error {
policy, err := localClient.GetEffectivePolicy(ctx, setting.DefaultScope())
if err != nil {
return err
}
printPolicySettings(policy)
return nil
}
func runSysPolicyReload(ctx context.Context, args []string) error {
policy, err := localClient.ReloadEffectivePolicy(ctx, setting.DefaultScope())
if err != nil {
return err
}
printPolicySettings(policy)
return nil
}
func printPolicySettings(policy *setting.Snapshot) {
if syspolicyArgs.json {
json, err := json.MarshalIndent(policy, "", "\t")
if err != nil {
errf("syspolicy marshalling error: %v", err)
} else {
outln(string(json))
}
return
}
if policy.Len() == 0 {
outln("No policy settings")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "Name\tOrigin\tValue\tError")
fmt.Fprintln(w, "----\t------\t-----\t-----")
for _, k := range slices.Sorted(policy.Keys()) {
setting, _ := policy.GetSetting(k)
var origin string
if o := setting.Origin(); o != nil {
origin = o.String()
}
if err := setting.Error(); err != nil {
fmt.Fprintf(w, "%s\t%s\t\t{%s}\n", k, origin, err)
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t\n", k, origin, setting.Value())
}
}
w.Flush()
fmt.Println()
return
}

View File

@@ -5,10 +5,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
@@ -86,6 +82,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
@@ -124,7 +121,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/client/web+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
@@ -148,6 +144,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/types/key+
tailscale.com/types/views from tailscale.com/tailcfg+
@@ -162,7 +159,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/groupmember from tailscale.com/client/web
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
@@ -174,14 +171,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/slicesx from tailscale.com/net/dns/recursive+
tailscale.com/util/syspolicy from tailscale.com/ipn
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
tailscale.com/util/usermetric from tailscale.com/health
tailscale.com/util/vizerror from tailscale.com/tailcfg+
W 💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate
W 💣 tailscale.com/util/winutil/gp from tailscale.com/util/syspolicy/source
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/client/web+
tailscale.com/version/distro from tailscale.com/client/web+
@@ -320,7 +321,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from archive/tar+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime/debug from github.com/coder/websocket/internal/xsync+
runtime/debug from tailscale.com+
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+

View File

@@ -79,10 +79,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
github.com/coder/websocket from tailscale.com/control/controlhttp+
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -249,6 +245,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
@@ -327,7 +324,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -364,6 +360,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/control/controlclient+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/tka+
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
@@ -381,7 +378,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+
@@ -401,8 +398,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/slicesx from tailscale.com/net/dns/recursive+
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
@@ -412,7 +412,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/vizerror from tailscale.com/tailcfg+
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+

View File

@@ -0,0 +1,30 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"testing"
"tailscale.com/tstest/deptest"
)
func TestOmitSSH(t *testing.T) {
const msg = "unexpected with ts_omit_ssh"
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
Tags: "ts_omit_ssh",
BadDeps: map[string]string{
"tailscale.com/ssh/tailssh": msg,
"golang.org/x/crypto/ssh": msg,
"tailscale.com/sessionrecording": msg,
"github.com/anmitsu/go-shlex": msg,
"github.com/creack/pty": msg,
"github.com/kr/fs": msg,
"github.com/pkg/sftp": msg,
"github.com/u-root/u-root/pkg/termios": msg,
"tempfork/gliderlabs/ssh": msg,
},
}.Check(t)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || darwin || freebsd || openbsd
//go:build (linux || darwin || freebsd || openbsd) && !ts_omit_ssh
package main

View File

@@ -788,7 +788,6 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
tfs, _ := sys.DriveForLocal.GetOK()
ret, err := netstack.Create(logf,
sys.Tun.Get(),
sys.Engine.Get(),
@@ -796,7 +795,6 @@ func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
sys.Dialer.Get(),
sys.DNSManager.Get(),
sys.ProxyMapper(),
tfs,
)
if err != nil {
return nil, err

View File

@@ -42,6 +42,7 @@ type testAttempt struct {
testName string // "TestFoo"
outcome string // "pass", "fail", "skip"
logs bytes.Buffer
start, end time.Time
isMarkedFlaky bool // set if the test is marked as flaky
issueURL string // set if the test is marked as flaky
@@ -132,11 +133,17 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
}
pkg := goOutput.Package
pkgTests := resultMap[pkg]
if pkgTests == nil {
pkgTests = make(map[string]*testAttempt)
resultMap[pkg] = pkgTests
}
if goOutput.Test == "" {
switch goOutput.Action {
case "start":
pkgTests[""] = &testAttempt{start: goOutput.Time}
case "fail", "pass", "skip":
for _, test := range pkgTests {
if test.outcome == "" {
if test.testName != "" && test.outcome == "" {
test.outcome = "fail"
ch <- test
}
@@ -144,15 +151,13 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
ch <- &testAttempt{
pkg: goOutput.Package,
outcome: goOutput.Action,
start: pkgTests[""].start,
end: goOutput.Time,
pkgFinished: true,
}
}
continue
}
if pkgTests == nil {
pkgTests = make(map[string]*testAttempt)
resultMap[pkg] = pkgTests
}
testName := goOutput.Test
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
testName = test
@@ -168,8 +173,10 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
pkgTests[testName] = &testAttempt{
pkg: pkg,
testName: testName,
start: goOutput.Time,
}
case "skip", "pass", "fail":
pkgTests[testName].end = goOutput.Time
pkgTests[testName].outcome = goOutput.Action
ch <- pkgTests[testName]
case "output":
@@ -213,7 +220,7 @@ func main() {
firstRun.tests = append(firstRun.tests, &packageTests{Pattern: pkg})
}
toRun := []*nextRun{firstRun}
printPkgOutcome := func(pkg, outcome string, attempt int) {
printPkgOutcome := func(pkg, outcome string, attempt int, runtime time.Duration) {
if outcome == "skip" {
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
return
@@ -225,10 +232,10 @@ func main() {
outcome = "FAIL"
}
if attempt > 1 {
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
fmt.Printf("%s\t%s\t%.3fs\t[attempt=%d]\n", outcome, pkg, runtime.Seconds(), attempt)
return
}
fmt.Printf("%s\t%s\n", outcome, pkg)
fmt.Printf("%s\t%s\t%.3fs\n", outcome, pkg, runtime.Seconds())
}
// Check for -coverprofile argument and filter it out
@@ -307,7 +314,7 @@ func main() {
// when a package times out.
failed = true
}
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt)
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start))
continue
}
if testingVerbose || tr.outcome == "fail" {

View File

@@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"sync"
"testing"
)
@@ -76,7 +77,10 @@ func TestFlakeRun(t *testing.T) {
t.Fatalf("go run . %s: %s with output:\n%s", testfile, err, out)
}
want := []byte("ok\t" + testfile + " [attempt=2]")
// Replace the unpredictable timestamp with "0.00s".
out = regexp.MustCompile(`\t\d+\.\d\d\ds\t`).ReplaceAll(out, []byte("\t0.00s\t"))
want := []byte("ok\t" + testfile + "\t0.00s\t[attempt=2]")
if !bytes.Contains(out, want) {
t.Fatalf("wanted output containing %q but got:\n%s", want, out)
}

View File

@@ -150,6 +150,7 @@ func runEsbuildServe(buildOptions esbuild.BuildOptions) {
log.Fatalf("Cannot start esbuild server: %v", err)
}
log.Printf("Listening on http://%s:%d\n", result.Host, result.Port)
select {}
}
func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {

View File

@@ -115,7 +115,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(), sys.ProxyMapper(), nil)
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)
}
@@ -272,8 +272,8 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
name = p.Hostinfo().Hostname()
}
addrs := make([]string, p.Addresses().Len())
for i := range p.Addresses().Len() {
addrs[i] = p.Addresses().At(i).Addr().String()
for i, ap := range p.Addresses().All() {
addrs[i] = ap.Addr().String()
}
return jsNetMapPeerNode{
jsNetMapNode: jsNetMapNode{
@@ -589,8 +589,8 @@ func mapSlice[T any, M any](a []T, f func(T) M) []M {
func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
n := make([]M, a.Len())
for i := range a.Len() {
n[i] = f(a.At(i))
for i, v := range a.All() {
n[i] = f(v)
}
return n
}

View File

@@ -564,6 +564,12 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
case opt.URL != "":
// Nothing.
case regen || persist.PrivateNodeKey.IsZero():
if regen {
c.logf("TEST: need to regenerate")
} else {
c.logf("TEST: private node key is zero, persist is %v", persist)
c.logf("TEST: private node key is zero, persist is %v", persist)
}
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
tryingNewKey = key.NewNode()

View File

@@ -15,7 +15,7 @@ import (
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/internal/noiseconn"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
@@ -201,7 +201,7 @@ func (up *Upgrader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return nil
}
cbConn, err := controlhttp.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
cbConn, err := controlhttpserver.AcceptHTTP(r.Context(), w, r, up.noiseKeyPriv, earlyWriteFn)
if err != nil {
up.logf("controlhttp: Accept: %v", err)
return

View File

@@ -38,6 +38,7 @@ import (
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dnscache"
@@ -571,9 +572,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
Method: "POST",
URL: u,
Header: http.Header{
"Upgrade": []string{upgradeHeaderValue},
"Connection": []string{"upgrade"},
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
"Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
"Connection": []string{"upgrade"},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
},
}
req = req.WithContext(ctx)
@@ -597,7 +598,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
return nil, fmt.Errorf("httptrace didn't provide a connection")
}
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue {
if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
resp.Body.Close()
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/wsconn"
)
@@ -42,11 +43,11 @@ func (d *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
// Can't set HTTP headers on the websocket request, so we have to to send
// the handshake via an HTTP header.
RawQuery: url.Values{
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
}.Encode(),
}
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
Subprotocols: []string{upgradeHeaderValue},
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
})
if err != nil {
return nil, err

View File

@@ -18,15 +18,6 @@ import (
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
upgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
handshakeHeaderName = "X-Tailscale-Handshake"
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"

View File

@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package controlhttpcommon contains common constants for used
// by the controlhttp client and controlhttpserver packages.
package controlhttpcommon
// UpgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
const UpgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
const HandshakeHeaderName = "X-Tailscale-Handshake"

View File

@@ -3,7 +3,8 @@
//go:build !ios
package controlhttp
// Package controlhttpserver contains the HTTP server side of the ts2021 control protocol.
package controlhttpserver
import (
"context"
@@ -18,6 +19,7 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
@@ -45,12 +47,12 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != upgradeHeaderValue {
if next != controlhttpcommon.UpgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
}
initB64 := r.Header.Get(handshakeHeaderName)
initB64 := r.Header.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
return nil, errors.New("no tailscale handshake header in HTTP request")
@@ -67,7 +69,7 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
return nil, errors.New("can't hijack client connection")
}
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
@@ -117,7 +119,7 @@ func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
@@ -129,7 +131,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != upgradeHeaderValue {
if c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
@@ -137,7 +139,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(handshakeHeaderName)
initB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")

View File

@@ -23,12 +23,15 @@ import (
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -158,7 +161,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
return err
}
}
conn, err := AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, earlyWriteFn)
if err != nil {
log.Print(err)
}
@@ -529,7 +532,7 @@ EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
func brokenMITMHandler(clock tstime.Clock) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
w.(http.Flusher).Flush()
@@ -574,7 +577,7 @@ func TestDialPlan(t *testing.T) {
close(done)
})
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := AcceptHTTP(context.Background(), w, r, server, nil)
conn, err := controlhttpserver.AcceptHTTP(context.Background(), w, r, server, nil)
if err != nil {
log.Print(err)
} else {
@@ -816,3 +819,14 @@ func (c *closeTrackConn) Close() error {
c.d.noteClose(c)
return c.Conn.Close()
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
// Only the controlhttpserver needs WebSockets...
"github.com/coder/websocket": "controlhttp client shouldn't need websockets",
},
}.Check(t)
}

View File

@@ -313,6 +313,9 @@ func (c *Client) preferIPv6() bool {
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
func useWebsockets() bool {
if !canWebsockets {
return false
}
if runtime.GOOS == "js" {
return true
}
@@ -383,7 +386,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
var node *tailcfg.DERPNode // nil when using c.url to dial
var idealNodeInRegion bool
switch {
case useWebsockets():
case canWebsockets && useWebsockets():
var urlStr string
if c.url != nil {
urlStr = c.url.String()

View File

@@ -17,7 +17,9 @@ import (
"tailscale.com/derp"
"tailscale.com/net/netmon"
"tailscale.com/tstest/deptest"
"tailscale.com/types/key"
"tailscale.com/util/set"
)
func TestSendRecv(t *testing.T) {
@@ -485,3 +487,23 @@ func TestProbe(t *testing.T) {
}
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
"github.com/coder/websocket": "shouldn't link websockets except on js/wasm",
},
}.Check(t)
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
Tags: "ts_debug_websockets",
WantDeps: set.Of(
"github.com/coder/websocket",
),
}.Check(t)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || js
//go:build js || ((linux || darwin) && ts_debug_websockets)
package derphttp
@@ -14,6 +14,8 @@ import (
"tailscale.com/net/wsconn"
)
const canWebsockets = true
func init() {
dialWebsocketFunc = dialWebsocket
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(js || ((linux || darwin) && ts_debug_websockets))
package derphttp
const canWebsockets = false

View File

@@ -411,7 +411,7 @@ func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION
// Kubernetes Operator components.
func App() string {
a := os.Getenv("TS_INTERNAL_APP")
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource {
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
return a
}
return ""

32
go.mod
View File

@@ -42,7 +42,7 @@ require (
github.com/golang/snappy v0.0.4
github.com/golangci/golangci-lint v1.57.1
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.18.0
github.com/google/go-containerregistry v0.20.2
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806
github.com/google/uuid v1.6.0
@@ -55,7 +55,7 @@ require (
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/jsimonetti/rtnetlink v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.4
github.com/klauspost/compress v1.17.11
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.20
@@ -80,7 +80,7 @@ require (
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1
@@ -100,8 +100,8 @@ require (
golang.org/x/mod v0.19.0
golang.org/x/net v0.27.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.22.0
golang.org/x/sync v0.9.0
golang.org/x/sys v0.27.0
golang.org/x/term v0.22.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.23.0
@@ -125,7 +125,7 @@ require (
github.com/Antonboom/testifylint v1.2.0 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
@@ -138,7 +138,7 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
github.com/dave/brenda v1.1.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghostiam/protogetter v0.3.5 // indirect
@@ -160,10 +160,10 @@ require (
github.com/ykadowak/zerologlint v0.1.5 // indirect
go-simpler.org/musttag v0.9.0 // indirect
go-simpler.org/sloglint v0.5.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
)
@@ -220,10 +220,10 @@ require (
github.com/daixiang0/gci v0.12.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/docker/cli v25.0.0+incompatible // indirect
github.com/docker/cli v27.3.1+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v26.1.4+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ettle/strcase v0.2.0 // indirect
@@ -322,7 +322,7 @@ require (
github.com/nunnatsa/ginkgolinter v0.16.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
@@ -376,7 +376,7 @@ require (
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.1.0 // indirect
github.com/uudashr/gocognit v1.1.2 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect

72
go.sum
View File

@@ -79,8 +79,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
@@ -277,16 +277,16 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpFowZBX6GoQ=
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
@@ -490,8 +490,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.18.0 h1:ShE7erKNPqRh5ue6Z9DUOlk04WsnFWPO6YGr3OxnfoQ=
github.com/google/go-containerregistry v0.18.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -627,8 +627,8 @@ github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8=
github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -749,8 +749,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
@@ -931,8 +931,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba h1:uNo1VCm/xg4alMkIKo8RWTKNx5y1otfVOcKbp+irkL4=
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba/go.mod h1:DxnqIXBplij66U2ZkL688xy07q97qQ83P+TVueLiHq4=
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10 h1:ZB47BgnHcEHQJODkDubs5ZiNeJxMhcgzefV3lykRwVQ=
github.com/tailscale/mkctr v0.0.0-20241111153353-1a38f6676f10/go.mod h1:iDx/0Rr9VV/KanSUDpJ6I/ROf0sQ7OqljXc/esl0UIA=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
@@ -981,8 +981,8 @@ github.com/ultraware/whitespace v0.1.0 h1:O1HKYoh0kIeqE8sFqZf1o0qbORXUCOQFrlaQyZ
github.com/ultraware/whitespace v0.1.0/go.mod h1:/se4r3beMFNmewJ4Xmz0nMQ941GJt+qmSHGP9emHYe0=
github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI=
github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
@@ -1022,20 +1022,20 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
@@ -1176,8 +1176,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1239,8 +1239,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@@ -25,7 +25,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -231,12 +231,12 @@ func desktop() (ret opt.Bool) {
}
seenDesktop := false
lineread.File("/proc/net/unix", func(line []byte) error {
for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
return nil
})
}
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
@@ -305,21 +305,21 @@ func inContainer() opt.Bool {
ret.Set(true)
return ret
}
lineread.File("/proc/1/cgroup", func(line []byte) error {
for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true)
return io.EOF // arbitrary non-nil error to stop loop
break
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
}
for lr := range lineiter.File("/proc/mounts") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true)
return io.EOF
break
}
return nil
})
}
return ret
}

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/types/ptr"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version/distro"
)
@@ -106,15 +106,18 @@ func linuxVersionMeta() (meta versionMeta) {
}
m := map[string]string{}
lineread.File(propFile, func(line []byte) error {
for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
eq := bytes.IndexByte(line, '=')
if eq == -1 {
return nil
continue
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v
return nil
})
}
if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v

View File

@@ -32,6 +32,8 @@ type ConfigVAlpha struct {
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
DisableSNAT opt.Bool `json:",omitempty"`
AppConnector *AppConnectorPrefs `json:",omitempty"` // advertise app connector; defaults to false (if nil or explicitly set to false)
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
NoStatefulFiltering opt.Bool `json:",omitempty"`
@@ -137,5 +139,9 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
mp.AutoUpdate = *c.AutoUpdate
mp.AutoUpdateSet = AutoUpdatePrefsMask{ApplySet: true, CheckSet: true}
}
if c.AppConnector != nil {
mp.AppConnector = *c.AppConnector
mp.AppConnectorSet = true
}
return mp, nil
}

View File

@@ -332,12 +332,10 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
}
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
sns, err := posture.GetSerialNumbers(b.logf)
res.SerialNumbers, err = posture.GetSerialNumbers(b.logf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
b.logf("c2n: GetSerialNumbers returned error: %v", err)
}
res.SerialNumbers = sns
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
// and looks good in client metrics, remove this parameter and always report MAC
@@ -352,6 +350,8 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
res.PostureDisabled = true
}
b.logf("c2n: posture identity disabled=%v reported %d serials %d hwaddrs", res.PostureDisabled, len(res.SerialNumbers), len(res.IfaceHardwareAddrs))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}

View File

@@ -354,9 +354,8 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
// Check that the peer is allowed to share with us.
addresses := peer.Addresses()
for i := range addresses.Len() {
addr := addresses.At(i)
capsMap := b.PeerCaps(addr.Addr())
for _, p := range addresses.All() {
capsMap := b.PeerCaps(p.Addr())
if capsMap.HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
return true
}

View File

@@ -399,11 +399,6 @@ type metrics struct {
// approvedRoutes is a metric that reports the number of network routes served by the local node and approved
// by the control server.
approvedRoutes *usermetric.Gauge
// primaryRoutes is a metric that reports the number of primary network routes served by the local node.
// A route being a primary route implies that the route is currently served by this node, and not by another
// subnet router in a high availability configuration.
primaryRoutes *usermetric.Gauge
}
// clientGen is a func that creates a control plane client.
@@ -454,8 +449,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
"tailscaled_advertised_routes", "Number of advertised network routes (e.g. by a subnet router)"),
approvedRoutes: sys.UserMetricsRegistry().NewGauge(
"tailscaled_approved_routes", "Number of approved network routes (e.g. by a subnet router)"),
primaryRoutes: sys.UserMetricsRegistry().NewGauge(
"tailscaled_primary_routes", "Number of network routes for which this node is a primary router (in high availability configuration)"),
}
b := &LocalBackend{
@@ -486,7 +479,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
mConn.SetNetInfoCallback(b.setNetInfo)
if sys.InitialConfig != nil {
if err := b.setConfigLocked(sys.InitialConfig); err != nil {
if err := b.initPrefsFromConfig(sys.InitialConfig); err != nil {
return nil, err
}
}
@@ -719,8 +712,8 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
// It returns (false, nil) if not running in declarative mode, (true, nil) on
// success, or (false, error) on failure.
func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
b.mu.Lock()
defer b.mu.Unlock()
unlock := b.lockAndGetUnlock()
defer unlock()
if b.conf == nil {
return false, nil
}
@@ -728,18 +721,21 @@ func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
if err != nil {
return false, err
}
if err := b.setConfigLocked(conf); err != nil {
if err := b.setConfigLockedOnEntry(conf, unlock); err != nil {
return false, fmt.Errorf("error setting config: %w", err)
}
return true, nil
}
func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
// TODO(irbekrm): notify the relevant components to consume any prefs
// updates. Currently only initial configfile settings are applied
// immediately.
// initPrefsFromConfig initializes the backend's prefs from the provided config.
// This should only be called once, at startup. For updates at runtime, use
// [LocalBackend.setConfigLocked].
func (b *LocalBackend) initPrefsFromConfig(conf *conffile.Config) error {
// TODO(maisem,bradfitz): combine this with setConfigLocked. This is called
// before anything is running, so there's no need to lock and we don't
// update any subsystems. At runtime, we both need to lock and update
// subsystems with the new prefs.
p := b.pm.CurrentPrefs().AsStruct()
mp, err := conf.Parsed.ToPrefs()
if err != nil {
@@ -749,13 +745,14 @@ func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
return err
}
b.setStaticEndpointsFromConfigLocked(conf)
b.conf = conf
return nil
}
defer func() {
b.conf = conf
}()
func (b *LocalBackend) setStaticEndpointsFromConfigLocked(conf *conffile.Config) {
if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) {
return nil
return
}
// Ensure that magicsock conn has the up to date static wireguard
@@ -769,6 +766,22 @@ func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
ms.SetStaticEndpoints(views.SliceOf(conf.Parsed.StaticEndpoints))
}
}
}
// setConfigLockedOnEntry uses the provided config to update the backend's prefs
// and other state.
func (b *LocalBackend) setConfigLockedOnEntry(conf *conffile.Config, unlock unlockOnce) error {
defer unlock()
p := b.pm.CurrentPrefs().AsStruct()
mp, err := conf.Parsed.ToPrefs()
if err != nil {
return fmt.Errorf("error parsing config to prefs: %w", err)
}
p.ApplyEdits(&mp)
b.setStaticEndpointsFromConfigLocked(conf)
b.setPrefsLockedOnEntry(p, unlock)
b.conf = conf
return nil
}
@@ -787,6 +800,19 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
}
// DisconnectControl shuts down control client. This can be run before node shutdown to force control to consider this ndoe
// inactive. This can be used to ensure that nodes that are HA subnet router or app connector replicas are shutting
// down, clients switch over to other replicas whilst the existing connections are kept alive for some period of time.
func (b *LocalBackend) DisconnectControl() {
b.mu.Lock()
defer b.mu.Unlock()
cc := b.resetControlClientLocked()
if cc == nil {
return
}
cc.Shutdown()
}
// captivePortalDetectionInterval is the duration to wait in an unhealthy state with connectivity broken
// before running captive portal detection.
const captivePortalDetectionInterval = 2 * time.Second
@@ -1785,8 +1811,7 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap, lastSuggestedExitNod
}
for _, peer := range nm.Peers {
for i := range peer.Addresses().Len() {
addr := peer.Addresses().At(i)
for _, addr := range peer.Addresses().All() {
if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
continue
}
@@ -1970,6 +1995,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
defer unlock()
if opts.UpdatePrefs != nil {
log.Printf("TESTPREFS: update prefs non-nil")
if err := b.checkPrefsLocked(opts.UpdatePrefs); err != nil {
return err
}
@@ -2036,6 +2062,10 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
prefs := b.pm.CurrentPrefs()
log.Printf("TESTPREFS persistent prefs: %v", prefs.Persist())
if s := prefs.Persist().AsStruct(); s != nil {
log.Printf("TESTPREFS persistent prefs private key is %v", s.PrivateNodeKey)
}
wantRunning := prefs.WantRunning()
if wantRunning {
if err := b.initMachineKeyLocked(); err != nil {
@@ -4971,8 +5001,8 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
case ipn.Running:
var addrStrs []string
addrs := netMap.GetAddresses()
for i := range addrs.Len() {
addrStrs = append(addrStrs, addrs.At(i).Addr().String())
for _, p := range addrs.All() {
addrStrs = append(addrStrs, p.Addr().String())
}
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrStrs, " "))
case ipn.NoState:
@@ -5477,7 +5507,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
// If there is no netmap, the client is going into a "turned off"
// state so reset the metrics.
b.metrics.approvedRoutes.Set(0)
b.metrics.primaryRoutes.Set(0)
return
}
@@ -5506,7 +5535,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
}
b.metrics.approvedRoutes.Set(approved)
b.metrics.primaryRoutes.Set(float64(tsaddr.WithoutExitRoute(nm.SelfNode.PrimaryRoutes()).Len()))
}
for _, p := range nm.Peers {
addNode(p)
@@ -6065,8 +6093,7 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo().Services()
for i := range svcs.Len() {
s := svcs.At(i)
for _, s := range svcs.All() {
switch s.Proto {
case tailcfg.PeerAPI4:
p4 = s.Port
@@ -6098,8 +6125,7 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
var have4, have6 bool
addrs := nm.GetAddresses()
for i := range addrs.Len() {
a := addrs.At(i)
for _, a := range addrs.All() {
if !a.IsSingleIP() {
continue
}
@@ -6121,10 +6147,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
}
func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr {
for i := range n.Addresses().Len() {
a := n.Addresses().At(i)
if a.IsSingleIP() && pred(a.Addr()) {
return a.Addr()
for _, pfx := range n.Addresses().All() {
if pfx.IsSingleIP() && pred(pfx.Addr()) {
return pfx.Addr()
}
}
return netip.Addr{}
@@ -6354,8 +6379,8 @@ func peerCanProxyDNS(p tailcfg.NodeView) bool {
// If p.Cap is not populated (e.g. older control server), then do the old
// thing of searching through services.
services := p.Hostinfo().Services()
for i := range services.Len() {
if s := services.At(i); s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
for _, s := range services.All() {
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
return true
}
}
@@ -6758,6 +6783,7 @@ func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
// NewProfile creates and switches to the new profile.
func (b *LocalBackend) NewProfile() error {
log.Printf("TESTPREFS: NewProfile LB")
unlock := b.lockAndGetUnlock()
defer unlock()

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"net/netip"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
@@ -32,6 +33,7 @@ import (
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/netcheck"
@@ -54,6 +56,8 @@ import (
"tailscale.com/util/must"
"tailscale.com/util/set"
"tailscale.com/util/syspolicy"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/util/syspolicy/source"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/wgcfg"
@@ -430,16 +434,25 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
}
func newTestLocalBackend(t testing.TB) *LocalBackend {
return newTestLocalBackendWithSys(t, new(tsd.System))
}
// newTestLocalBackendWithSys creates a new LocalBackend with the given tsd.System.
// If the state store or engine are not set in sys, they will be set to a new
// in-memory store and fake userspace engine, respectively.
func newTestLocalBackendWithSys(t testing.TB, sys *tsd.System) *LocalBackend {
var logf logger.Logf = logger.Discard
sys := new(tsd.System)
store := new(mem.Store)
sys.Set(store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
if _, ok := sys.StateStore.GetOK(); !ok {
sys.Set(new(mem.Store))
}
if _, ok := sys.Engine.GetOK(); !ok {
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
t.Cleanup(eng.Close)
sys.Set(eng)
}
t.Cleanup(eng.Close)
sys.Set(eng)
lb, err := NewLocalBackend(logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatalf("NewLocalBackend: %v", err)
@@ -1559,94 +1572,6 @@ func dnsResponse(domain, address string) []byte {
return must.Get(b.Finish())
}
type errorSyspolicyHandler struct {
t *testing.T
err error
key syspolicy.Key
allowKeys map[syspolicy.Key]*string
}
func (h *errorSyspolicyHandler) ReadString(key string) (string, error) {
sk := syspolicy.Key(key)
if _, ok := h.allowKeys[sk]; !ok {
h.t.Errorf("ReadString: %q is not in list of permitted keys", h.key)
}
if sk == h.key {
return "", h.err
}
return "", syspolicy.ErrNoSuchKey
}
func (h *errorSyspolicyHandler) ReadUInt64(key string) (uint64, error) {
h.t.Errorf("ReadUInt64(%q) unexpectedly called", key)
return 0, syspolicy.ErrNoSuchKey
}
func (h *errorSyspolicyHandler) ReadBoolean(key string) (bool, error) {
h.t.Errorf("ReadBoolean(%q) unexpectedly called", key)
return false, syspolicy.ErrNoSuchKey
}
func (h *errorSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
h.t.Errorf("ReadStringArray(%q) unexpectedly called", key)
return nil, syspolicy.ErrNoSuchKey
}
type mockSyspolicyHandler struct {
t *testing.T
// stringPolicies is the collection of policies that we expect to see
// queried by the current test. If the policy is expected but unset, then
// use nil, otherwise use a string equal to the policy's desired value.
stringPolicies map[syspolicy.Key]*string
// stringArrayPolicies is the collection of policies that we expected to see
// queries by the current test, that return policy string arrays.
stringArrayPolicies map[syspolicy.Key][]string
// failUnknownPolicies is set if policies other than those in stringPolicies
// (uint64 or bool policies are not supported by mockSyspolicyHandler yet)
// should be considered a test failure if they are queried.
failUnknownPolicies bool
}
func (h *mockSyspolicyHandler) ReadString(key string) (string, error) {
if s, ok := h.stringPolicies[syspolicy.Key(key)]; ok {
if s == nil {
return "", syspolicy.ErrNoSuchKey
}
return *s, nil
}
if h.failUnknownPolicies {
h.t.Errorf("ReadString(%q) unexpectedly called", key)
}
return "", syspolicy.ErrNoSuchKey
}
func (h *mockSyspolicyHandler) ReadUInt64(key string) (uint64, error) {
if h.failUnknownPolicies {
h.t.Errorf("ReadUInt64(%q) unexpectedly called", key)
}
return 0, syspolicy.ErrNoSuchKey
}
func (h *mockSyspolicyHandler) ReadBoolean(key string) (bool, error) {
if h.failUnknownPolicies {
h.t.Errorf("ReadBoolean(%q) unexpectedly called", key)
}
return false, syspolicy.ErrNoSuchKey
}
func (h *mockSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
if h.failUnknownPolicies {
h.t.Errorf("ReadStringArray(%q) unexpectedly called", key)
}
if s, ok := h.stringArrayPolicies[syspolicy.Key(key)]; ok {
if s == nil {
return []string{}, syspolicy.ErrNoSuchKey
}
return s, nil
}
return nil, syspolicy.ErrNoSuchKey
}
func TestSetExitNodeIDPolicy(t *testing.T) {
pfx := netip.MustParsePrefix
tests := []struct {
@@ -1856,23 +1781,18 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
},
}
syspolicy.RegisterWellKnownSettingsForTest(t)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
b := newTestBackend(t)
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: map[syspolicy.Key]*string{
syspolicy.ExitNodeID: nil,
syspolicy.ExitNodeIP: nil,
},
}
if test.exitNodeIDKey {
msh.stringPolicies[syspolicy.ExitNodeID] = &test.exitNodeID
}
if test.exitNodeIPKey {
msh.stringPolicies[syspolicy.ExitNodeIP] = &test.exitNodeIP
}
syspolicy.SetHandlerForTest(t, msh)
policyStore := source.NewTestStoreOf(t,
source.TestSettingOf(syspolicy.ExitNodeID, test.exitNodeID),
source.TestSettingOf(syspolicy.ExitNodeIP, test.exitNodeIP),
)
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
if test.nm == nil {
test.nm = new(netmap.NetworkMap)
}
@@ -1994,13 +1914,13 @@ func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
report: report,
},
}
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: map[syspolicy.Key]*string{
syspolicy.ExitNodeID: ptr.To("auto:any"),
},
}
syspolicy.SetHandlerForTest(t, msh)
syspolicy.RegisterWellKnownSettingsForTest(t)
policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
syspolicy.ExitNodeID, "auto:any",
))
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := newTestLocalBackend(t)
@@ -2049,13 +1969,11 @@ func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
}
cc = newClient(t, opts)
b.cc = cc
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: map[syspolicy.Key]*string{
syspolicy.ExitNodeID: ptr.To("auto:any"),
},
}
syspolicy.SetHandlerForTest(t, msh)
syspolicy.RegisterWellKnownSettingsForTest(t)
policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
syspolicy.ExitNodeID, "auto:any",
))
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
peer1 := makePeer(1, withCap(26), withDERP(3), withSuggest(), withExitRoutes())
peer2 := makePeer(2, withCap(26), withDERP(2), withSuggest(), withExitRoutes())
selfNode := tailcfg.Node{
@@ -2160,13 +2078,11 @@ func TestSetControlClientStatusAutoExitNode(t *testing.T) {
DERPMap: derpMap,
}
b := newTestLocalBackend(t)
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: map[syspolicy.Key]*string{
syspolicy.ExitNodeID: ptr.To("auto:any"),
},
}
syspolicy.SetHandlerForTest(t, msh)
syspolicy.RegisterWellKnownSettingsForTest(t)
policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
syspolicy.ExitNodeID, "auto:any",
))
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
b.netMap = nm
b.lastSuggestedExitNode = peer1.StableID()
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, report)
@@ -2400,17 +2316,16 @@ func TestApplySysPolicy(t *testing.T) {
},
}
syspolicy.RegisterWellKnownSettingsForTest(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: make(map[syspolicy.Key]*string, len(tt.stringPolicies)),
}
settings := make([]source.TestSetting[string], 0, len(tt.stringPolicies))
for p, v := range tt.stringPolicies {
v := v // construct a unique pointer for each policy value
msh.stringPolicies[p] = &v
settings = append(settings, source.TestSettingOf(p, v))
}
syspolicy.SetHandlerForTest(t, msh)
policyStore := source.NewTestStoreOf(t, settings...)
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
t.Run("unit", func(t *testing.T) {
prefs := tt.prefs.Clone()
@@ -2546,35 +2461,19 @@ func TestPreferencePolicyInfo(t *testing.T) {
},
}
syspolicy.RegisterWellKnownSettingsForTest(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, pp := range preferencePolicies {
t.Run(string(pp.key), func(t *testing.T) {
var h syspolicy.Handler
allPolicies := make(map[syspolicy.Key]*string, len(preferencePolicies)+1)
allPolicies[syspolicy.ControlURL] = nil
for _, pp := range preferencePolicies {
allPolicies[pp.key] = nil
s := source.TestSetting[string]{
Key: pp.key,
Error: tt.policyError,
Value: tt.policyValue,
}
if tt.policyError != nil {
h = &errorSyspolicyHandler{
t: t,
err: tt.policyError,
key: pp.key,
allowKeys: allPolicies,
}
} else {
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: allPolicies,
failUnknownPolicies: true,
}
msh.stringPolicies[pp.key] = &tt.policyValue
h = msh
}
syspolicy.SetHandlerForTest(t, h)
policyStore := source.NewTestStoreOf(t, s)
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
prefs := defaultPrefs.AsStruct()
pp.set(prefs, tt.initialValue)
@@ -3142,12 +3041,10 @@ func deterministicNodeForTest(t testing.TB, want views.Slice[tailcfg.StableNodeI
var ret tailcfg.NodeView
gotIDs := make([]tailcfg.StableNodeID, got.Len())
for i := range got.Len() {
nv := got.At(i)
for i, nv := range got.All() {
if !nv.Valid() {
t.Fatalf("invalid node at index %v", i)
}
gotIDs[i] = nv.StableID()
if nv.StableID() == use {
ret = nv
@@ -3825,15 +3722,16 @@ func TestShouldAutoExitNode(t *testing.T) {
expectedBool: false,
},
}
syspolicy.RegisterWellKnownSettingsForTest(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msh := &mockSyspolicyHandler{
t: t,
stringPolicies: map[syspolicy.Key]*string{
syspolicy.ExitNodeID: ptr.To(tt.exitNodeIDPolicyValue),
},
}
syspolicy.SetHandlerForTest(t, msh)
policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
syspolicy.ExitNodeID, tt.exitNodeIDPolicyValue,
))
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
got := shouldAutoExitNode()
if got != tt.expectedBool {
t.Fatalf("expected %v got %v for %v policy value", tt.expectedBool, got, tt.exitNodeIDPolicyValue)
@@ -3971,17 +3869,13 @@ func TestFillAllowedSuggestions(t *testing.T) {
want: []tailcfg.StableNodeID{"ABC", "def", "gHiJ"},
},
}
syspolicy.RegisterWellKnownSettingsForTest(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mh := mockSyspolicyHandler{
t: t,
}
if tt.allowPolicy != nil {
mh.stringArrayPolicies = map[syspolicy.Key][]string{
syspolicy.AllowedSuggestedExitNodes: tt.allowPolicy,
}
}
syspolicy.SetHandlerForTest(t, &mh)
policyStore := source.NewTestStoreOf(t, source.TestSettingOf(
syspolicy.AllowedSuggestedExitNodes, tt.allowPolicy,
))
syspolicy.MustRegisterStoreForTest(t, "TestStore", setting.DeviceScope, policyStore)
got := fillAllowedSuggestions()
if got == nil {
@@ -4538,3 +4432,35 @@ func TestLoginNotifications(t *testing.T) {
})
}
}
// TestConfigFileReload tests that the LocalBackend reloads its configuration
// when the configuration file changes.
func TestConfigFileReload(t *testing.T) {
cfg1 := `{"Hostname": "foo", "Version": "alpha0"}`
f := filepath.Join(t.TempDir(), "cfg")
must.Do(os.WriteFile(f, []byte(cfg1), 0600))
sys := new(tsd.System)
sys.InitialConfig = must.Get(conffile.Load(f))
lb := newTestLocalBackendWithSys(t, sys)
must.Do(lb.Start(ipn.Options{}))
lb.mu.Lock()
hn := lb.hostinfo.Hostname
lb.mu.Unlock()
if hn != "foo" {
t.Fatalf("got %q; want %q", hn, "foo")
}
cfg2 := `{"Hostname": "bar", "Version": "alpha0"}`
must.Do(os.WriteFile(f, []byte(cfg2), 0600))
if !must.Get(lb.ReloadConfig()) {
t.Fatal("reload failed")
}
lb.mu.Lock()
hn = lb.hostinfo.Hostname
lb.mu.Unlock()
if hn != "bar" {
t.Fatalf("got %q; want %q", hn, "bar")
}
}

View File

@@ -430,8 +430,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
}
bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
for i := range persist.DisallowedTKAStateIDs().Len() {
stateID := persist.DisallowedTKAStateIDs().At(i)
for _, stateID := range persist.DisallowedTKAStateIDs().All() {
if stateID == bootstrapStateID {
return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
}
@@ -572,8 +571,7 @@ func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
NodeKey: p.Key(),
}
for i := range p.Addresses().Len() {
addr := p.Addresses().At(i)
for _, addr := range p.Addresses().All() {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
}

View File

@@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"runtime"
"slices"
"strings"
@@ -203,6 +204,7 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
// Reset unloads the current profile, if any.
func (pm *profileManager) Reset() {
log.Printf("TESTPREFS: Reset")
pm.currentUserID = ""
pm.NewProfile()
}
@@ -215,6 +217,7 @@ func (pm *profileManager) Reset() {
// is logged into so that we can keep track of things like their domain name
// across user switches to disambiguate the same account but a different tailnet.
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
log.Printf("TESTPREFS: SetPrefs with prefs %v", prefsIn)
cp := pm.currentProfile
if persist := prefsIn.Persist(); !persist.Valid() || persist.NodeID() == "" || persist.UserProfile().LoginName == "" {
// We don't know anything about this profile, so ignore it for now.
@@ -223,6 +226,7 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
// Check if we already have an existing profile that matches the user/node.
if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 {
log.Printf("TESTPREFS: SetPrefs found existing profile")
// We already have a profile for this user/node we should reuse it. Also
// cleanup any other duplicate profiles.
cp = existing[0]
@@ -230,6 +234,7 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
for _, p := range existing {
// Clear the state.
if err := pm.store.WriteState(p.Key, nil); err != nil {
log.Printf("TESTPREFS: SetPrefs found existing profile, error writing state: %v", err)
// We couldn't delete the state, so keep the profile around.
continue
}
@@ -237,6 +242,8 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
// in [profileManager.setProfilePrefs] below.
delete(pm.knownProfiles, p.ID)
}
} else {
log.Printf("TESTPREFS: SetPrefs not found existing profile")
}
pm.currentProfile = cp
if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
@@ -327,6 +334,7 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
// profile, such as verifying the caller's access rights or checking
// if another profile for the same node already exists.
func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error {
log.Printf("TESTPREFS: setProfilePrefsNoPerm")
isCurrentProfile := pm.currentProfile == profile
if isCurrentProfile {
pm.prefs = clonedPrefs
@@ -423,6 +431,7 @@ func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, erro
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
// If the profile does not exist, it returns an [errProfileNotFound].
func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
log.Printf("TESTPREFS: SwitchProfile")
metricSwitchProfile.Add(1)
kp, ok := pm.knownProfiles[id]
@@ -450,6 +459,7 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
// It creates a new one and switches to it if the current user does not have a default profile,
// or returns an error if the default profile is inaccessible or could not be loaded.
func (pm *profileManager) SwitchToDefaultProfile() error {
log.Printf("TESTPREFS: SwitchToDefault")
if id := pm.DefaultUserProfileID(pm.currentUserID); id != "" {
return pm.SwitchProfile(id)
}
@@ -547,6 +557,7 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
}
func (pm *profileManager) deleteCurrentProfile() error {
log.Printf("TESTPREFS: deleteCurrent")
if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
return err
}
@@ -627,6 +638,7 @@ func (pm *profileManager) NewProfile() {
// NewProfileForUser is like [profileManager.NewProfile], but it switches to the
// specified user and sets that user as the profile owner for the new profile.
func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
log.Printf("TESTPREFS: NewProfileForUser")
pm.currentUserID = uid
metricNewProfile.Add(1)
@@ -641,6 +653,7 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
// newly created profile immediately. It returns the newly created profile on success,
// or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) {
log.Printf("TESTPREFS: newProfileWithPrefs")
metricNewProfile.Add(1)
profile := &ipn.LoginProfile{LocalUserID: uid}
@@ -733,6 +746,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
if err != nil {
return nil, err
}
log.Printf("TESTPREFS: newProfileWithGOOS with state key %v", stateKey)
knownProfiles, err := readKnownProfiles(store)
if err != nil {
@@ -748,12 +762,15 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
}
if stateKey != "" {
log.Printf("TESTPREFS: state key %v exists", stateKey)
for _, v := range knownProfiles {
log.Printf("TESTPREFS: state key %v exists looking at matching profile %s", stateKey, v)
if v.Key == stateKey {
pm.currentProfile = v
}
}
if pm.currentProfile == nil {
log.Printf("TESTPREFS: current profile is nil")
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf)
}
@@ -776,12 +793,14 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
// uid passed in from the unix tests. The uid's used for Windows tests
// and runtime must be valid Windows security identifier structures.
} else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" {
log.Printf("TESTPREFS: no known profiles")
// No known profiles, try a migration.
pm.dlogf("no known profiles; trying to migrate from legacy prefs")
if _, err := pm.migrateFromLegacyPrefs(pm.currentUserID, true); err != nil {
return nil, err
}
} else {
log.Printf("TESTPREFS: newProfileWithGOOS new profile")
pm.NewProfile()
}

View File

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

View File

@@ -27,7 +27,7 @@ import (
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/mak"
)
@@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
for line := range lineiter.Bytes(out) {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
continue
}
add(string(line))
return nil
})
}
default:
lineread.File("/etc/passwd", func(line []byte) error {
for lr := range lineiter.File("/etc/passwd") {
line, err := lr.Value()
if err != nil {
break
}
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
continue
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
continue
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
}
return res, nil
}

View File

@@ -121,8 +121,8 @@ func (b *LocalBackend) updateWebClientListenersLocked() {
}
addrs := b.netMap.GetAddresses()
for i := range addrs.Len() {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort)
for _, pfx := range addrs.All() {
addrPort := netip.AddrPortFrom(pfx.Addr(), webClientPort)
if _, ok := b.webClientListeners[addrPort]; ok {
continue // already listening
}

View File

@@ -62,7 +62,8 @@ import (
"tailscale.com/util/osdiag"
"tailscale.com/util/progresstracking"
"tailscale.com/util/rands"
"tailscale.com/util/testenv"
"tailscale.com/util/syspolicy/rsop"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/version"
"tailscale.com/wgengine/magicsock"
)
@@ -77,6 +78,7 @@ var handler = map[string]localAPIHandler{
"cert/": (*Handler).serveCert,
"file-put/": (*Handler).serveFilePut,
"files/": (*Handler).serveFiles,
"policy/": (*Handler).servePolicy,
"profiles/": (*Handler).serveProfiles,
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
@@ -98,6 +100,7 @@ var handler = map[string]localAPIHandler{
"derpmap": (*Handler).serveDERPMap,
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"dial": (*Handler).serveDial,
"disconnect-control": (*Handler).disconnectControl,
"dns-osconfig": (*Handler).serveDNSOSConfig,
"dns-query": (*Handler).serveDNSQuery,
"drive/fileserver-address": (*Handler).serveDriveServerAddr,
@@ -570,15 +573,9 @@ func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
clientmetric.WritePrometheusExpositionFormat(w)
}
// TODO(kradalby): Remove this once we have landed on a final set of
// metrics to export to clients and consider the metrics stable.
var debugUsermetricsEndpoint = envknob.RegisterBool("TS_DEBUG_USER_METRICS")
// serveUserMetrics returns user-facing metrics in Prometheus text
// exposition format.
func (h *Handler) serveUserMetrics(w http.ResponseWriter, r *http.Request) {
if !testenv.InTest() && !debugUsermetricsEndpoint() {
http.Error(w, "usermetrics debug flag not enabled", http.StatusForbidden)
return
}
h.b.UserMetricsRegistry().Handler(w, r)
}
@@ -956,6 +953,22 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
servePprofFunc(w, r)
}
// disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that
// node no longer communicates with control. Doing this makes control consider this node inactive. This can be used
// before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the
// peers to switch over to another replica whilst still maintaining th existing peer connections.
func (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
h.b.DisconnectControl()
}
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "access denied", http.StatusForbidden)
@@ -1339,6 +1352,53 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
e.Encode(prefs)
}
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "policy access denied", http.StatusForbidden)
return
}
suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/policy/")
if !ok {
http.Error(w, "misconfigured", http.StatusInternalServerError)
return
}
var scope setting.PolicyScope
if suffix == "" {
scope = setting.DefaultScope()
} else if err := scope.UnmarshalText([]byte(suffix)); err != nil {
http.Error(w, fmt.Sprintf("%q is not a valid scope", suffix), http.StatusBadRequest)
return
}
policy, err := rsop.PolicyFor(scope)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var effectivePolicy *setting.Snapshot
switch r.Method {
case "GET":
effectivePolicy = policy.Get()
case "POST":
effectivePolicy, err = policy.Reload()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
default:
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(effectivePolicy)
}
type resJSON struct {
Error string `json:",omitempty"`
}

View File

@@ -7,25 +7,34 @@ package kubestore
import (
"context"
"fmt"
"log"
"net"
"os"
"strings"
"time"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/kube/kubeapi"
"tailscale.com/kube/kubeclient"
"tailscale.com/types/logger"
)
// TODO(irbekrm): should we bump this? should we have retries? See tailscale/tailscale#13024
const timeout = 5 * time.Second
// Store is an ipn.StateStore that uses a Kubernetes Secret for persistence.
type Store struct {
client kubeclient.Client
canPatch bool
secretName string
// memory holds the latest tailscale state. Writes write state to a kube Secret and memory, Reads read from
// memory.
memory mem.Store
}
// New returns a new Store that persists to the named secret.
// New returns a new Store that persists to the named Secret.
func New(_ logger.Logf, secretName string) (*Store, error) {
c, err := kubeclient.New()
if err != nil {
@@ -39,11 +48,16 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
if err != nil {
return nil, err
}
return &Store{
s := &Store{
client: c,
canPatch: canPatch,
secretName: secretName,
}, nil
}
// Load latest state from kube Secret if it already exists.
if err := s.loadState(); err != nil && err != ipn.ErrStateNotExist {
return nil, fmt.Errorf("error loading state from kube Secret: %w", err)
}
return s, nil
}
func (s *Store) SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error)) {
@@ -54,37 +68,17 @@ func (s *Store) String() string { return "kube.Store" }
// ReadState implements the StateStore interface.
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
secret, err := s.client.GetSecret(ctx, s.secretName)
if err != nil {
if st, ok := err.(*kubeapi.Status); ok && st.Code == 404 {
return nil, ipn.ErrStateNotExist
}
return nil, err
}
b, ok := secret.Data[sanitizeKey(id)]
if !ok {
return nil, ipn.ErrStateNotExist
}
return b, nil
}
func sanitizeKey(k ipn.StateKey) string {
// The only valid characters in a Kubernetes secret key are alphanumeric, -,
// _, and .
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
return r
}
return '_'
}, string(k))
return s.memory.ReadState(ipn.StateKey(sanitizeKey(id)))
}
// WriteState implements the StateStore interface.
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func (s *Store) WriteState(id ipn.StateKey, bs []byte) (err error) {
defer func() {
if err == nil {
s.memory.WriteState(ipn.StateKey(sanitizeKey(id)), bs)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
secret, err := s.client.GetSecret(ctx, s.secretName)
@@ -137,3 +131,30 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
}
return err
}
func (s *Store) loadState() error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
secret, err := s.client.GetSecret(ctx, s.secretName)
if err != nil {
if st, ok := err.(*kubeapi.Status); ok && st.Code == 404 {
return ipn.ErrStateNotExist
}
return err
}
log.Printf("TEST: kube store: got secret: %#+v", secret.Data)
s.memory.LoadFromMap(secret.Data)
return nil
}
func sanitizeKey(k ipn.StateKey) string {
// The only valid characters in a Kubernetes secret key are alphanumeric, -,
// _, and .
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
return r
}
return '_'
}, string(k))
}

View File

@@ -7,10 +7,13 @@ package mem
import (
"bytes"
"encoding/json"
"log"
"sync"
xmaps "golang.org/x/exp/maps"
"tailscale.com/ipn"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
)
// New returns a new Store.
@@ -28,18 +31,23 @@ type Store struct {
func (s *Store) String() string { return "mem.Store" }
// ReadState implements the StateStore interface.
// It returns ipn.ErrStateNotExist if the state does not exist.
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
log.Printf("TEST: ReadState key %v ", id)
s.mu.Lock()
defer s.mu.Unlock()
bs, ok := s.cache[id]
if !ok {
return nil, ipn.ErrStateNotExist
}
log.Printf("TEST: ReadState key %v val %v", id, string(bs))
return bs, nil
}
// WriteState implements the StateStore interface.
// It never returns an error.
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
log.Printf("TEST: WriteState key %v ", id)
s.mu.Lock()
defer s.mu.Unlock()
if s.cache == nil {
@@ -49,6 +57,21 @@ func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
return nil
}
// LoadFromMap loads the in-memory cache from the provided map.
// Any existing content is cleared, and the provided map is
// copied into the cache.
func (s *Store) LoadFromMap(m map[string][]byte) {
log.Printf("Store: LoadFromMap")
s.mu.Lock()
defer s.mu.Unlock()
xmaps.Clear(s.cache)
for k, v := range m {
log.Printf("TEST: setting state key %v %+#v", k, string(v))
mak.Set(&s.cache, ipn.StateKey(k), v)
}
return
}
// LoadFromJSON attempts to unmarshal json content into the
// in-memory cache.
func (s *Store) LoadFromJSON(data []byte) error {

View File

@@ -21,6 +21,22 @@
#### AppConnector
AppConnector defines a Tailscale app connector node configured via Connector.
_Appears in:_
- [ConnectorSpec](#connectorspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `routes` _[Routes](#routes)_ | Routes are optional preconfigured routes for the domains routed via the app connector.<br />If not set, routes for the domains will be discovered dynamically.<br />If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may<br />also dynamically discover other routes.<br />https://tailscale.com/kb/1332/apps-best-practices#preconfiguration | | Format: cidr <br />MinItems: 1 <br />Type: string <br /> |
#### Connector
@@ -86,8 +102,9 @@ _Appears in:_
| `tags` _[Tags](#tags)_ | Tags that the Tailscale node will be tagged with.<br />Defaults to [tag:k8s].<br />To autoapprove the subnet routes or exit node defined by a Connector,<br />you can configure Tailscale ACLs to give these tags the necessary<br />permissions.<br />See https://tailscale.com/kb/1337/acl-syntax#autoapprovers.<br />If you specify custom tags here, you must also make the operator an owner of these tags.<br />See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator.<br />Tags cannot be changed once a Connector node has been created.<br />Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$. | | Pattern: `^tag:[a-zA-Z][a-zA-Z0-9-]*$` <br />Type: string <br /> |
| `hostname` _[Hostname](#hostname)_ | Hostname is the tailnet hostname that should be assigned to the<br />Connector node. If unset, hostname defaults to <connector<br />name>-connector. Hostname can contain lower case letters, numbers and<br />dashes, it must not start or end with a dash and must be between 2<br />and 63 characters long. | | Pattern: `^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$` <br />Type: string <br /> |
| `proxyClass` _string_ | ProxyClass is the name of the ProxyClass custom resource that<br />contains configuration options that should be applied to the<br />resources created for this Connector. If unset, the operator will<br />create resources with the default configuration. | | |
| `subnetRouter` _[SubnetRouter](#subnetrouter)_ | SubnetRouter defines subnet routes that the Connector node should<br />expose to tailnet. If unset, none are exposed.<br />https://tailscale.com/kb/1019/subnets/ | | |
| `exitNode` _boolean_ | ExitNode defines whether the Connector node should act as a<br />Tailscale exit node. Defaults to false.<br />https://tailscale.com/kb/1103/exit-nodes | | |
| `subnetRouter` _[SubnetRouter](#subnetrouter)_ | SubnetRouter defines subnet routes that the Connector device should<br />expose to tailnet as a Tailscale subnet router.<br />https://tailscale.com/kb/1019/subnets/<br />If this field is unset, the device does not get configured as a Tailscale subnet router.<br />This field is mutually exclusive with the appConnector field. | | |
| `appConnector` _[AppConnector](#appconnector)_ | AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is<br />configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the<br />Connector does not act as an app connector.<br />Note that you will need to manually configure the permissions and the domains for the app connector via the<br />Admin panel.<br />Note also that the main tested and supported use case of this config option is to deploy an app connector on<br />Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose<br />cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have<br />tested or optimised for.<br />If you are using the app connector to access SaaS applications because you need a predictable egress IP that<br />can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows<br />via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT<br />device with a static IP address.<br />https://tailscale.com/kb/1281/app-connectors | | |
| `exitNode` _boolean_ | ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.<br />This field is mutually exclusive with the appConnector field.<br />https://tailscale.com/kb/1103/exit-nodes | | |
#### ConnectorStatus
@@ -106,6 +123,7 @@ _Appears in:_
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#condition-v1-meta) array_ | List of status conditions to indicate the status of the Connector.<br />Known condition types are `ConnectorReady`. | | |
| `subnetRoutes` _string_ | SubnetRoutes are the routes currently exposed to tailnet via this<br />Connector instance. | | |
| `isExitNode` _boolean_ | IsExitNode is set to true if the Connector acts as an exit node. | | |
| `isAppConnector` _boolean_ | IsAppConnector is set to true if the Connector acts as an app connector. | | |
| `tailnetIPs` _string array_ | TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6)<br />assigned to the Connector node. | | |
| `hostname` _string_ | Hostname is the fully qualified domain name of the Connector node.<br />If MagicDNS is enabled in your tailnet, it is the MagicDNS name of the<br />node. | | |
@@ -381,6 +399,7 @@ _Appears in:_
| `nodeName` _string_ | Proxy Pod's node name.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | |
| `nodeSelector` _object (keys:string, values:string)_ | Proxy Pod's node selector.<br />By default Tailscale Kubernetes operator does not apply any node<br />selector.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | |
| `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#toleration-v1-core) array_ | Proxy Pod's tolerations.<br />By default Tailscale Kubernetes operator does not apply any<br />tolerations.<br />https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling | | |
| `topologySpreadConstraints` _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#topologyspreadconstraint-v1-core) array_ | Proxy Pod's topology spread constraints.<br />By default Tailscale Kubernetes operator does not apply any topology spread constraints.<br />https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ | | |
#### ProxyClass
@@ -745,6 +764,7 @@ _Validation:_
- Type: string
_Appears in:_
- [AppConnector](#appconnector)
- [SubnetRouter](#subnetrouter)

View File

@@ -22,6 +22,7 @@ var ConnectorKind = "Connector"
// +kubebuilder:resource:scope=Cluster,shortName=cn
// +kubebuilder:printcolumn:name="SubnetRoutes",type="string",JSONPath=`.status.subnetRoutes`,description="CIDR ranges exposed to tailnet by a subnet router defined via this Connector instance."
// +kubebuilder:printcolumn:name="IsExitNode",type="string",JSONPath=`.status.isExitNode`,description="Whether this Connector instance defines an exit node."
// +kubebuilder:printcolumn:name="IsAppConnector",type="string",JSONPath=`.status.isAppConnector`,description="Whether this Connector instance is an app connector."
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type == "ConnectorReady")].reason`,description="Status of the deployed Connector resources."
// Connector defines a Tailscale node that will be deployed in the cluster. The
@@ -55,7 +56,8 @@ type ConnectorList struct {
}
// ConnectorSpec describes a Tailscale node to be deployed in the cluster.
// +kubebuilder:validation:XValidation:rule="has(self.subnetRouter) || self.exitNode == true",message="A Connector needs to be either an exit node or a subnet router, or both."
// +kubebuilder:validation:XValidation:rule="has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true) || has(self.appConnector)",message="A Connector needs to have at least one of exit node, subnet router or app connector configured."
// +kubebuilder:validation:XValidation:rule="!((has(self.subnetRouter) || (has(self.exitNode) && self.exitNode == true)) && has(self.appConnector))",message="The appConnector field is mutually exclusive with exitNode and subnetRouter fields."
type ConnectorSpec struct {
// Tags that the Tailscale node will be tagged with.
// Defaults to [tag:k8s].
@@ -82,13 +84,31 @@ type ConnectorSpec struct {
// create resources with the default configuration.
// +optional
ProxyClass string `json:"proxyClass,omitempty"`
// SubnetRouter defines subnet routes that the Connector node should
// expose to tailnet. If unset, none are exposed.
// SubnetRouter defines subnet routes that the Connector device should
// expose to tailnet as a Tailscale subnet router.
// https://tailscale.com/kb/1019/subnets/
// If this field is unset, the device does not get configured as a Tailscale subnet router.
// This field is mutually exclusive with the appConnector field.
// +optional
SubnetRouter *SubnetRouter `json:"subnetRouter"`
// ExitNode defines whether the Connector node should act as a
// Tailscale exit node. Defaults to false.
SubnetRouter *SubnetRouter `json:"subnetRouter,omitempty"`
// AppConnector defines whether the Connector device should act as a Tailscale app connector. A Connector that is
// configured as an app connector cannot be a subnet router or an exit node. If this field is unset, the
// Connector does not act as an app connector.
// Note that you will need to manually configure the permissions and the domains for the app connector via the
// Admin panel.
// Note also that the main tested and supported use case of this config option is to deploy an app connector on
// Kubernetes to access SaaS applications available on the public internet. Using the app connector to expose
// cluster workloads or other internal workloads to tailnet might work, but this is not a use case that we have
// tested or optimised for.
// If you are using the app connector to access SaaS applications because you need a predictable egress IP that
// can be whitelisted, it is also your responsibility to ensure that cluster traffic from the connector flows
// via that predictable IP, for example by enforcing that cluster egress traffic is routed via an egress NAT
// device with a static IP address.
// https://tailscale.com/kb/1281/app-connectors
// +optional
AppConnector *AppConnector `json:"appConnector,omitempty"`
// ExitNode defines whether the Connector device should act as a Tailscale exit node. Defaults to false.
// This field is mutually exclusive with the appConnector field.
// https://tailscale.com/kb/1103/exit-nodes
// +optional
ExitNode bool `json:"exitNode"`
@@ -104,6 +124,17 @@ type SubnetRouter struct {
AdvertiseRoutes Routes `json:"advertiseRoutes"`
}
// AppConnector defines a Tailscale app connector node configured via Connector.
type AppConnector struct {
// Routes are optional preconfigured routes for the domains routed via the app connector.
// If not set, routes for the domains will be discovered dynamically.
// If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
// also dynamically discover other routes.
// https://tailscale.com/kb/1332/apps-best-practices#preconfiguration
// +optional
Routes Routes `json:"routes"`
}
type Tags []Tag
func (tags Tags) Stringify() []string {
@@ -156,6 +187,9 @@ type ConnectorStatus struct {
// IsExitNode is set to true if the Connector acts as an exit node.
// +optional
IsExitNode bool `json:"isExitNode"`
// IsAppConnector is set to true if the Connector acts as an app connector.
// +optional
IsAppConnector bool `json:"isAppConnector"`
// TailnetIPs is the set of tailnet IP addresses (both IPv4 and IPv6)
// assigned to the Connector node.
// +optional

View File

@@ -154,7 +154,11 @@ type Pod struct {
// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#scheduling
// +optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
// Proxy Pod's topology spread constraints.
// By default Tailscale Kubernetes operator does not apply any topology spread constraints.
// https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
// +optional
TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`
}
type Metrics struct {

View File

@@ -13,6 +13,26 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AppConnector) DeepCopyInto(out *AppConnector) {
*out = *in
if in.Routes != nil {
in, out := &in.Routes, &out.Routes
*out = make(Routes, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppConnector.
func (in *AppConnector) DeepCopy() *AppConnector {
if in == nil {
return nil
}
out := new(AppConnector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Connector) DeepCopyInto(out *Connector) {
*out = *in
@@ -85,6 +105,11 @@ func (in *ConnectorSpec) DeepCopyInto(out *ConnectorSpec) {
*out = new(SubnetRouter)
(*in).DeepCopyInto(*out)
}
if in.AppConnector != nil {
in, out := &in.AppConnector, &out.AppConnector
*out = new(AppConnector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectorSpec.
@@ -392,6 +417,13 @@ func (in *Pod) DeepCopyInto(out *Pod) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.TopologySpreadConstraints != nil {
in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints
*out = make([]corev1.TopologySpreadConstraint, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pod.

View File

@@ -5,12 +5,14 @@ package kubetypes
const (
// Hostinfo App values for the Tailscale Kubernetes Operator components.
AppOperator = "k8s-operator"
AppAPIServerProxy = "k8s-operator-proxy"
AppIngressProxy = "k8s-operator-ingress-proxy"
AppIngressResource = "k8s-operator-ingress-resource"
AppEgressProxy = "k8s-operator-egress-proxy"
AppConnector = "k8s-operator-connector-resource"
AppOperator = "k8s-operator"
AppAPIServerProxy = "k8s-operator-proxy"
AppIngressProxy = "k8s-operator-ingress-proxy"
AppIngressResource = "k8s-operator-ingress-resource"
AppEgressProxy = "k8s-operator-egress-proxy"
AppConnector = "k8s-operator-connector-resource"
AppProxyGroupEgress = "k8s-operator-proxygroup-egress"
AppProxyGroupIngress = "k8s-operator-proxygroup-ingress"
// Clientmetrics for Tailscale Kubernetes Operator components
MetricIngressProxyCount = "k8s_ingress_proxies" // L3
@@ -19,8 +21,10 @@ const (
MetricConnectorResourceCount = "k8s_connector_resources"
MetricConnectorWithSubnetRouterCount = "k8s_connector_subnetrouter_resources"
MetricConnectorWithExitNodeCount = "k8s_connector_exitnode_resources"
MetricConnectorWithAppConnectorCount = "k8s_connector_appconnector_resources"
MetricNameserverCount = "k8s_nameserver_resources"
MetricRecorderCount = "k8s_recorder_resources"
MetricEgressServiceCount = "k8s_egress_service_resources"
MetricProxyGroupCount = "k8s_proxygroup_resources"
MetricProxyGroupEgressCount = "k8s_proxygroup_egress_resources"
MetricProxyGroupIngressCount = "k8s_proxygroup_ingress_resources"
)

View File

@@ -73,13 +73,13 @@ See also the dependencies in the [Tailscale CLI][].
- [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/ae6ca9944745/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.28.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.8.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.26.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.25.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.19.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))

View File

@@ -57,23 +57,23 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/4d49adab4de7/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/52804fd3056a/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/6580b55d49ca/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/8865133fd3ef/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/28f7e73c7afb/LICENSE))
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/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/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/ae6ca9944745/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.28.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5: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.18.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.19.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.8.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.26.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.25.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.19.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))

View File

@@ -57,6 +57,7 @@ func (m *resolvdManager) SetDNS(config OSConfig) error {
if len(newSearch) > 1 {
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
newResolvConf = append(newResolvConf, '\n')
}
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
@@ -123,6 +124,6 @@ func (m resolvdManager) readResolvConf() (config OSConfig, err error) {
}
func removeSearchLines(orig []byte) []byte {
re := regexp.MustCompile(`(?m)^search\s+.+$`)
re := regexp.MustCompile(`(?ms)^search\s+.+$`)
return re.ReplaceAll(orig, []byte(""))
}

View File

@@ -27,6 +27,7 @@ import (
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
)
@@ -276,6 +277,8 @@ func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, on
tb.Fatal("cannot skip both UDP and TCP servers")
}
logf := tstest.WhileTestRunningLogger(tb)
tcpResponse := make([]byte, len(response)+2)
binary.BigEndian.PutUint16(tcpResponse, uint16(len(response)))
copy(tcpResponse[2:], response)
@@ -329,13 +332,13 @@ func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, on
// Read the length header, then the buffer
var length uint16
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
tb.Logf("error reading length header: %v", err)
logf("error reading length header: %v", err)
return
}
req := make([]byte, length)
n, err := io.ReadFull(conn, req)
if err != nil {
tb.Logf("error reading query: %v", err)
logf("error reading query: %v", err)
return
}
req = req[:n]
@@ -343,7 +346,7 @@ func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, on
// Write response
if _, err := conn.Write(tcpResponse); err != nil {
tb.Logf("error writing response: %v", err)
logf("error writing response: %v", err)
return
}
}
@@ -367,7 +370,7 @@ func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, on
handleUDP := func(addr netip.AddrPort, req []byte) {
onRequest(false, req)
if _, err := udpLn.WriteToUDPAddrPort(response, addr); err != nil {
tb.Logf("error writing response: %v", err)
logf("error writing response: %v", err)
}
}
@@ -390,7 +393,7 @@ func runDNSServer(tb testing.TB, opts *testDNSServerOptions, response []byte, on
tb.Cleanup(func() {
tcpLn.Close()
udpLn.Close()
tb.Logf("waiting for listeners to finish...")
logf("waiting for listeners to finish...")
wg.Wait()
})
return
@@ -450,7 +453,8 @@ func makeLargeResponse(tb testing.TB, domain string) (request, response []byte)
}
func runTestQuery(tb testing.TB, request []byte, modify func(*forwarder), ports ...uint16) ([]byte, error) {
netMon, err := netmon.New(tb.Logf)
logf := tstest.WhileTestRunningLogger(tb)
netMon, err := netmon.New(logf)
if err != nil {
tb.Fatal(err)
}
@@ -458,7 +462,7 @@ func runTestQuery(tb testing.TB, request []byte, modify func(*forwarder), ports
var dialer tsdial.Dialer
dialer.SetNetMon(netMon)
fwd := newForwarder(tb.Logf, netMon, nil, &dialer, new(health.Tracker), nil)
fwd := newForwarder(logf, netMon, nil, &dialer, new(health.Tracker), nil)
if modify != nil {
modify(fwd)
}

View File

@@ -82,8 +82,8 @@ func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool
pathForTest("bart")
// Built a bart table.
t := &bart.Table[struct{}]{}
for i := range addrs.Len() {
t.Insert(addrs.At(i), struct{}{})
for _, p := range addrs.All() {
t.Insert(p, struct{}{})
}
return bartLookup(t)
}
@@ -99,8 +99,8 @@ func NewContainsIPFunc(addrs views.Slice[netip.Prefix]) func(ip netip.Addr) bool
// General case:
pathForTest("ip-map")
m := set.Set[netip.Addr]{}
for i := range addrs.Len() {
m.Add(addrs.At(i).Addr())
for _, p := range addrs.All() {
m.Add(p.Addr())
}
return ipInMap(m)
}

View File

@@ -392,10 +392,11 @@ type probePlan map[string][]probe
// sortRegions returns the regions of dm first sorted
// from fastest to slowest (based on the 'last' report),
// end in regions that have no data.
func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion) {
func sortRegions(dm *tailcfg.DERPMap, last *Report, preferredDERP int) (prev []*tailcfg.DERPRegion) {
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
for _, reg := range dm.Regions {
if reg.Avoid {
// include an otherwise avoid region if it is the current preferred region
if reg.Avoid && reg.RegionID != preferredDERP {
continue
}
prev = append(prev, reg)
@@ -420,9 +421,19 @@ func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion)
// a full report, all regions are scanned.)
const numIncrementalRegions = 3
// makeProbePlan generates the probe plan for a DERPMap, given the most
// recent report and whether IPv6 is configured on an interface.
func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report) (plan probePlan) {
// makeProbePlan generates the probe plan for a DERPMap, given the most recent
// report and the current home DERP. preferredDERP is passed independently of
// last (report) because last is currently nil'd to indicate a desire for a full
// netcheck.
//
// TODO(raggi,jwhited): refactor the callers and this function to be more clear
// about full vs. incremental netchecks, and remove the need for the history
// hiding. This was avoided in an incremental change due to exactly this kind of
// distant coupling.
// TODO(raggi): change from "preferred DERP" from a historical report to "home
// DERP" as in what DERP is the current home connection, this would further
// reduce flap events.
func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report, preferredDERP int) (plan probePlan) {
if last == nil || len(last.RegionLatency) == 0 {
return makeProbePlanInitial(dm, ifState)
}
@@ -433,9 +444,34 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report) (pl
had4 := len(last.RegionV4Latency) > 0
had6 := len(last.RegionV6Latency) > 0
hadBoth := have6if && had4 && had6
for ri, reg := range sortRegions(dm, last) {
if ri == numIncrementalRegions {
break
// #13969 ensure that the home region is always probed.
// If a netcheck has unstable latency, such as a user with large amounts of
// bufferbloat or a highly congested connection, there are cases where a full
// netcheck may observe a one-off high latency to the current home DERP. Prior
// to the forced inclusion of the home DERP, this would result in an
// incremental netcheck following such an event to cause a home DERP move, with
// restoration back to the home DERP on the next full netcheck ~5 minutes later
// - which is highly disruptive when it causes shifts in geo routed subnet
// routers. By always including the home DERP in the incremental netcheck, we
// ensure that the home DERP is always probed, even if it observed a recenet
// poor latency sample. This inclusion enables the latency history checks in
// home DERP selection to still take effect.
// planContainsHome indicates whether the home DERP has been added to the probePlan,
// if there is no prior home, then there's no home to additionally include.
planContainsHome := preferredDERP == 0
for ri, reg := range sortRegions(dm, last, preferredDERP) {
regIsHome := reg.RegionID == preferredDERP
if ri >= numIncrementalRegions {
// planned at least numIncrementalRegions regions and that includes the
// last home region (or there was none), plan complete.
if planContainsHome {
break
}
// planned at least numIncrementalRegions regions, but not the home region,
// check if this is the home region, if not, skip it.
if !regIsHome {
continue
}
}
var p4, p6 []probe
do4 := have4if
@@ -446,7 +482,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report) (pl
tries := 1
isFastestTwo := ri < 2
if isFastestTwo {
if isFastestTwo || regIsHome {
tries = 2
} else if hadBoth {
// For dual stack machines, make the 3rd & slower nodes alternate
@@ -457,14 +493,15 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *netmon.State, last *Report) (pl
do4, do6 = false, true
}
}
if !isFastestTwo && !had6 {
if !regIsHome && !isFastestTwo && !had6 {
do6 = false
}
if reg.RegionID == last.PreferredDERP {
if regIsHome {
// But if we already had a DERP home, try extra hard to
// make sure it's there so we don't flip flop around.
tries = 4
planContainsHome = true
}
for try := 0; try < tries; try++ {
@@ -789,9 +826,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
c.curState = rs
last := c.last
// Even if we're doing a non-incremental update, we may want to try our
// preferred DERP region for captive portal detection. Save that, if we
// have it.
// Extract preferredDERP from the last report, if available. This will be used
// in captive portal detection and DERP flapping suppression. Ideally this would
// be the current active home DERP rather than the last report preferred DERP,
// but only the latter is presently available.
var preferredDERP int
if last != nil {
preferredDERP = last.PreferredDERP
@@ -848,7 +886,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap, opts *GetRe
var plan probePlan
if opts == nil || !opts.OnlyTCP443 {
plan = makeProbePlan(dm, ifState, last)
plan = makeProbePlan(dm, ifState, last, preferredDERP)
}
// If we're doing a full probe, also check for a captive portal. We

View File

@@ -357,6 +357,15 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
wantPrevLen: 3,
wantDERP: 2, // moved to d2 since d1 is gone
},
{
name: "preferred_derp_hysteresis_no_switch_pct",
steps: []step{
{0 * time.Second, report("d1", 34*time.Millisecond, "d2", 35*time.Millisecond)},
{1 * time.Second, report("d1", 34*time.Millisecond, "d2", 23*time.Millisecond)},
},
wantPrevLen: 2,
wantDERP: 1, // diff is 11ms, but d2 is greater than 2/3s of d1
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -590,6 +599,40 @@ func TestMakeProbePlan(t *testing.T) {
"region-3-v4": []probe{p("3a", 4)},
},
},
{
// #13969: ensure that the prior/current home region is always included in
// probe plans, so that we don't flap between regions due to a single major
// netcheck having excluded the home region due to a spuriously high sample.
name: "ensure_home_region_inclusion",
dm: basicMap,
have6if: true,
last: &Report{
RegionLatency: map[int]time.Duration{
1: 50 * time.Millisecond,
2: 20 * time.Millisecond,
3: 30 * time.Millisecond,
4: 40 * time.Millisecond,
},
RegionV4Latency: map[int]time.Duration{
1: 50 * time.Millisecond,
2: 20 * time.Millisecond,
},
RegionV6Latency: map[int]time.Duration{
3: 30 * time.Millisecond,
4: 40 * time.Millisecond,
},
PreferredDERP: 1,
},
want: probePlan{
"region-1-v4": []probe{p("1a", 4), p("1a", 4, 60*ms), p("1a", 4, 220*ms), p("1a", 4, 330*ms)},
"region-1-v6": []probe{p("1a", 6), p("1a", 6, 60*ms), p("1a", 6, 220*ms), p("1a", 6, 330*ms)},
"region-2-v4": []probe{p("2a", 4), p("2b", 4, 24*ms)},
"region-2-v6": []probe{p("2a", 6), p("2b", 6, 24*ms)},
"region-3-v4": []probe{p("3a", 4), p("3b", 4, 36*ms)},
"region-3-v6": []probe{p("3a", 6), p("3b", 6, 36*ms)},
"region-4-v4": []probe{p("4a", 4)},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -597,7 +640,11 @@ func TestMakeProbePlan(t *testing.T) {
HaveV6: tt.have6if,
HaveV4: !tt.no4,
}
got := makeProbePlan(tt.dm, ifState, tt.last)
preferredDERP := 0
if tt.last != nil {
preferredDERP = tt.last.PreferredDERP
}
got := makeProbePlan(tt.dm, ifState, tt.last, preferredDERP)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unexpected plan; got:\n%v\nwant:\n%v\n", got, tt.want)
}
@@ -770,7 +817,7 @@ func TestSortRegions(t *testing.T) {
report.RegionLatency[3] = time.Second * time.Duration(6)
report.RegionLatency[4] = time.Second * time.Duration(0)
report.RegionLatency[5] = time.Second * time.Duration(2)
sortedMap := sortRegions(unsortedMap, report)
sortedMap := sortRegions(unsortedMap, report, 0)
// Sorting by latency this should result in rid: 5, 2, 1, 3
// rid 4 with latency 0 should be at the end

View File

@@ -5,7 +5,6 @@ package netmon
import (
"bytes"
"errors"
"log"
"net/netip"
"os/exec"
@@ -15,7 +14,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
var (
@@ -34,11 +33,6 @@ func init() {
var procNetRouteErr atomic.Bool
// errStopReading is a sentinel error value used internally by
// lineread.File callers to stop reading. It doesn't escape to
// callers/users.
var errStopReading = errors.New("stop reading")
/*
Parse 10.0.0.1 out of:
@@ -54,44 +48,42 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error {
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
lineNum++
if lineNum == 1 {
// Skip header line.
return nil
continue
}
if lineNum > maxProcNetRouteRead {
return errStopReading
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
return nil
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
return errStopReading
break
}
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with
@@ -144,23 +136,26 @@ func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
return
}
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
lineread.Reader(out, func(line []byte) error {
for lr := range lineiter.Reader(out) {
line, err := lr.Value()
if err != nil {
break
}
const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
return nil
continue
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
return nil
continue
}
ipb := line[:sp]
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
ret = ip
log.Printf("interfaces: found Android default route %v", ip)
}
return nil
})
}
cmd.Process.Kill()
cmd.Wait()
return ret, netip.Addr{}, ret.IsValid()

View File

@@ -4,14 +4,13 @@
package netmon
import (
"errors"
"io"
"net/netip"
"os/exec"
"testing"
"go4.org/mem"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version"
)
@@ -73,31 +72,34 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
var f []mem.RO
lineread.Reader(stdout, func(lineb []byte) error {
for lr := range lineiter.Reader(stdout) {
lineb, err := lr.Value()
if err != nil {
break
}
line := mem.B(lineb)
if !mem.Contains(line, mem.S("default")) {
return nil
continue
}
f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") {
return nil
continue
}
ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) {
return nil
continue
}
if mem.Contains(flagsm, mem.S("I")) {
return nil
continue
}
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() {
ret = ip
netif = netifm.StringCopy()
// We've found what we're looking for.
return errStopReadingNetstatTable
break
}
return nil
})
}
return ret, netif, ret.IsValid()
}
@@ -110,5 +112,3 @@ func TestFetchRoutingTable(t *testing.T) {
}
}
}
var errStopReadingNetstatTable = errors.New("found private gateway")

View File

@@ -23,7 +23,7 @@ import (
"go4.org/mem"
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
func init() {
@@ -32,11 +32,6 @@ func init() {
var procNetRouteErr atomic.Bool
// errStopReading is a sentinel error value used internally by
// lineread.File callers to stop reading. It doesn't escape to
// callers/users.
var errStopReading = errors.New("stop reading")
/*
Parse 10.0.0.1 out of:
@@ -52,44 +47,42 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
}
lineNum := 0
var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error {
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
return ret, myIP, false
}
lineNum++
if lineNum == 1 {
// Skip header line.
return nil
continue
}
if lineNum > maxProcNetRouteRead {
return errStopReading
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
return nil
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
return nil // ignore error, skip line and keep going
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
return errStopReading
break
}
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with

View File

@@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package netmon
import (

View File

@@ -81,6 +81,12 @@ const (
addrTypeNotSupported replyCode = 8
)
// UDP conn default buffer size and read timeout.
const (
bufferSize = 8 * 1024
readTimeout = 5 * time.Second
)
// Server is a SOCKS5 proxy server.
type Server struct {
// Logf optionally specifies the logger to use.
@@ -143,7 +149,8 @@ type Conn struct {
clientConn net.Conn
request *request
udpClientAddr net.Addr
udpClientAddr net.Addr
udpTargetConns map[socksAddr]net.Conn
}
// Run starts the new connection.
@@ -276,15 +283,6 @@ func (c *Conn) handleUDP() error {
}
defer clientUDPConn.Close()
serverUDPConn, err := net.ListenPacket("udp", "[::]:0")
if err != nil {
res := errorResponse(generalFailure)
buf, _ := res.marshal()
c.clientConn.Write(buf)
return err
}
defer serverUDPConn.Close()
bindAddr, bindPort, err := splitHostPort(clientUDPConn.LocalAddr().String())
if err != nil {
return err
@@ -305,25 +303,32 @@ func (c *Conn) handleUDP() error {
}
c.clientConn.Write(buf)
return c.transferUDP(c.clientConn, clientUDPConn, serverUDPConn)
return c.transferUDP(c.clientConn, clientUDPConn)
}
func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, targetConn net.PacketConn) error {
func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const bufferSize = 8 * 1024
const readTimeout = 5 * time.Second
// client -> target
go func() {
defer cancel()
c.udpTargetConns = make(map[socksAddr]net.Conn)
// close all target udp connections when the client connection is closed
defer func() {
for _, conn := range c.udpTargetConns {
_ = conn.Close()
}
}()
buf := make([]byte, bufferSize)
for {
select {
case <-ctx.Done():
return
default:
err := c.handleUDPRequest(clientConn, targetConn, buf, readTimeout)
err := c.handleUDPRequest(ctx, clientConn, buf)
if err != nil {
if isTimeout(err) {
continue
@@ -337,29 +342,6 @@ func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, ta
}
}()
// target -> client
go func() {
defer cancel()
buf := make([]byte, bufferSize)
for {
select {
case <-ctx.Done():
return
default:
err := c.handleUDPResponse(targetConn, clientConn, buf, readTimeout)
if err != nil {
if isTimeout(err) {
continue
}
if errors.Is(err, net.ErrClosed) {
return
}
c.logf("udp transfer: handle udp response fail: %v", err)
}
}
}
}()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
_, err := io.Copy(io.Discard, associatedTCP)
@@ -369,11 +351,50 @@ func (c *Conn) transferUDP(associatedTCP net.Conn, clientConn net.PacketConn, ta
return err
}
func (c *Conn) handleUDPRequest(
func (c *Conn) getOrDialTargetConn(
ctx context.Context,
clientConn net.PacketConn,
targetAddr socksAddr,
) (net.Conn, error) {
conn, exist := c.udpTargetConns[targetAddr]
if exist {
return conn, nil
}
conn, err := c.srv.dial(ctx, "udp", targetAddr.hostPort())
if err != nil {
return nil, err
}
c.udpTargetConns[targetAddr] = conn
// target -> client
go func() {
buf := make([]byte, bufferSize)
for {
select {
case <-ctx.Done():
return
default:
err := c.handleUDPResponse(clientConn, targetAddr, conn, buf)
if err != nil {
if isTimeout(err) {
continue
}
if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) {
return
}
c.logf("udp transfer: handle udp response fail: %v", err)
}
}
}
}()
return conn, nil
}
func (c *Conn) handleUDPRequest(
ctx context.Context,
clientConn net.PacketConn,
targetConn net.PacketConn,
buf []byte,
readTimeout time.Duration,
) error {
// add a deadline for the read to avoid blocking forever
_ = clientConn.SetReadDeadline(time.Now().Add(readTimeout))
@@ -386,38 +407,35 @@ func (c *Conn) handleUDPRequest(
if err != nil {
return fmt.Errorf("parse udp request: %w", err)
}
targetAddr, err := net.ResolveUDPAddr("udp", req.addr.hostPort())
targetConn, err := c.getOrDialTargetConn(ctx, clientConn, req.addr)
if err != nil {
c.logf("resolve target addr fail: %v", err)
return fmt.Errorf("dial target %s fail: %w", req.addr, err)
}
nn, err := targetConn.WriteTo(data, targetAddr)
nn, err := targetConn.Write(data)
if err != nil {
return fmt.Errorf("write to target %s fail: %w", targetAddr, err)
return fmt.Errorf("write to target %s fail: %w", req.addr, err)
}
if nn != len(data) {
return fmt.Errorf("write to target %s fail: %w", targetAddr, io.ErrShortWrite)
return fmt.Errorf("write to target %s fail: %w", req.addr, io.ErrShortWrite)
}
return nil
}
func (c *Conn) handleUDPResponse(
targetConn net.PacketConn,
clientConn net.PacketConn,
targetAddr socksAddr,
targetConn net.Conn,
buf []byte,
readTimeout time.Duration,
) error {
// add a deadline for the read to avoid blocking forever
_ = targetConn.SetReadDeadline(time.Now().Add(readTimeout))
n, addr, err := targetConn.ReadFrom(buf)
n, err := targetConn.Read(buf)
if err != nil {
return fmt.Errorf("read from target: %w", err)
}
host, port, err := splitHostPort(addr.String())
if err != nil {
return fmt.Errorf("split host port: %w", err)
}
hdr := udpRequest{addr: socksAddr{addrType: getAddrType(host), addr: host, port: port}}
hdr := udpRequest{addr: targetAddr}
pkt, err := hdr.marshal()
if err != nil {
return fmt.Errorf("marshal udp request: %w", err)
@@ -627,10 +645,15 @@ func (s socksAddr) marshal() ([]byte, error) {
pkt = binary.BigEndian.AppendUint16(pkt, s.port)
return pkt, nil
}
func (s socksAddr) hostPort() string {
return net.JoinHostPort(s.addr, strconv.Itoa(int(s.port)))
}
func (s socksAddr) String() string {
return s.hostPort()
}
// response contains the contents of
// a response packet sent from the proxy
// to the client.

View File

@@ -169,12 +169,25 @@ func TestReadPassword(t *testing.T) {
func TestUDP(t *testing.T) {
// backend UDP server which we'll use SOCKS5 to connect to
listener, err := net.ListenPacket("udp", ":0")
if err != nil {
t.Fatal(err)
newUDPEchoServer := func() net.PacketConn {
listener, err := net.ListenPacket("udp", ":0")
if err != nil {
t.Fatal(err)
}
go udpEchoServer(listener)
return listener
}
backendServerPort := listener.LocalAddr().(*net.UDPAddr).Port
go udpEchoServer(listener)
const echoServerNumber = 3
echoServerListener := make([]net.PacketConn, echoServerNumber)
for i := 0; i < echoServerNumber; i++ {
echoServerListener[i] = newUDPEchoServer()
}
defer func() {
for i := 0; i < echoServerNumber; i++ {
_ = echoServerListener[i].Close()
}
}()
// SOCKS5 server
socks5, err := net.Listen("tcp", ":0")
@@ -184,84 +197,93 @@ func TestUDP(t *testing.T) {
socks5Port := socks5.Addr().(*net.TCPAddr).Port
go socks5Server(socks5)
// net/proxy don't support UDP, so we need to manually send the SOCKS5 UDP request
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", socks5Port))
if err != nil {
t.Fatal(err)
}
_, err = conn.Write([]byte{0x05, 0x01, 0x00}) // client hello with no auth
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf) // server hello
if err != nil {
t.Fatal(err)
}
if n != 2 || buf[0] != 0x05 || buf[1] != 0x00 {
t.Fatalf("got: %q want: 0x05 0x00", buf[:n])
// make a socks5 udpAssociate conn
newUdpAssociateConn := func() (socks5Conn net.Conn, socks5UDPAddr socksAddr) {
// net/proxy don't support UDP, so we need to manually send the SOCKS5 UDP request
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", socks5Port))
if err != nil {
t.Fatal(err)
}
_, err = conn.Write([]byte{socks5Version, 0x01, noAuthRequired}) // client hello with no auth
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf) // server hello
if err != nil {
t.Fatal(err)
}
if n != 2 || buf[0] != socks5Version || buf[1] != noAuthRequired {
t.Fatalf("got: %q want: 0x05 0x00", buf[:n])
}
targetAddr := socksAddr{addrType: ipv4, addr: "0.0.0.0", port: 0}
targetAddrPkt, err := targetAddr.marshal()
if err != nil {
t.Fatal(err)
}
_, err = conn.Write(append([]byte{socks5Version, byte(udpAssociate), 0x00}, targetAddrPkt...)) // client reqeust
if err != nil {
t.Fatal(err)
}
n, err = conn.Read(buf) // server response
if err != nil {
t.Fatal(err)
}
if n < 3 || !bytes.Equal(buf[:3], []byte{socks5Version, 0x00, 0x00}) {
t.Fatalf("got: %q want: 0x05 0x00 0x00", buf[:n])
}
udpProxySocksAddr, err := parseSocksAddr(bytes.NewReader(buf[3:n]))
if err != nil {
t.Fatal(err)
}
return conn, udpProxySocksAddr
}
targetAddr := socksAddr{
addrType: domainName,
addr: "localhost",
port: uint16(backendServerPort),
}
targetAddrPkt, err := targetAddr.marshal()
if err != nil {
t.Fatal(err)
}
_, err = conn.Write(append([]byte{0x05, 0x03, 0x00}, targetAddrPkt...)) // client reqeust
if err != nil {
t.Fatal(err)
}
conn, udpProxySocksAddr := newUdpAssociateConn()
defer conn.Close()
n, err = conn.Read(buf) // server response
if err != nil {
t.Fatal(err)
}
if n < 3 || !bytes.Equal(buf[:3], []byte{0x05, 0x00, 0x00}) {
t.Fatalf("got: %q want: 0x05 0x00 0x00", buf[:n])
}
udpProxySocksAddr, err := parseSocksAddr(bytes.NewReader(buf[3:n]))
if err != nil {
t.Fatal(err)
sendUDPAndWaitResponse := func(socks5UDPConn net.Conn, addr socksAddr, body []byte) (responseBody []byte) {
udpPayload, err := (&udpRequest{addr: addr}).marshal()
if err != nil {
t.Fatal(err)
}
udpPayload = append(udpPayload, body...)
_, err = socks5UDPConn.Write(udpPayload)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := socks5UDPConn.Read(buf)
if err != nil {
t.Fatal(err)
}
_, responseBody, err = parseUDPRequest(buf[:n])
if err != nil {
t.Fatal(err)
}
return responseBody
}
udpProxyAddr, err := net.ResolveUDPAddr("udp", udpProxySocksAddr.hostPort())
if err != nil {
t.Fatal(err)
}
udpConn, err := net.DialUDP("udp", nil, udpProxyAddr)
socks5UDPConn, err := net.DialUDP("udp", nil, udpProxyAddr)
if err != nil {
t.Fatal(err)
}
udpPayload, err := (&udpRequest{addr: targetAddr}).marshal()
if err != nil {
t.Fatal(err)
}
udpPayload = append(udpPayload, []byte("Test")...)
_, err = udpConn.Write(udpPayload) // send udp package
if err != nil {
t.Fatal(err)
}
n, _, err = udpConn.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
_, responseBody, err := parseUDPRequest(buf[:n]) // read udp response
if err != nil {
t.Fatal(err)
}
if string(responseBody) != "Test" {
t.Fatalf("got: %q want: Test", responseBody)
}
err = udpConn.Close()
if err != nil {
t.Fatal(err)
}
err = conn.Close()
if err != nil {
t.Fatal(err)
defer socks5UDPConn.Close()
for i := 0; i < echoServerNumber; i++ {
port := echoServerListener[i].LocalAddr().(*net.UDPAddr).Port
addr := socksAddr{addrType: ipv4, addr: "127.0.0.1", port: uint16(port)}
requestBody := []byte(fmt.Sprintf("Test %d", i))
responseBody := sendUDPAndWaitResponse(socks5UDPConn, addr, requestBody)
if !bytes.Equal(requestBody, responseBody) {
t.Fatalf("got: %q want: %q", responseBody, requestBody)
}
}
}

View File

@@ -279,7 +279,13 @@ func setNetMon(netMon *netmon.Monitor) {
if ifName == "" {
return
}
ifIndex := state.Interface[ifName].Index
// DefaultRouteInterface and Interface are gathered at different points in time.
// Check for existence first, to avoid a nil pointer dereference.
iface, ok := state.Interface[ifName]
if !ok {
return
}
ifIndex := iface.Index
sockStats.mu.Lock()
defer sockStats.mu.Unlock()
// Ignore changes to unknown interfaces -- it would require

View File

@@ -180,8 +180,7 @@ func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() }
// IPv6 /0 route.
func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
var v4, v6 bool
for i := range rr.Len() {
r := rr.At(i)
for _, r := range rr.All() {
if r == allIPv4 {
v4 = true
} else if r == allIPv6 {
@@ -194,8 +193,8 @@ func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
// ContainsExitRoute reports whether rr contains at least one of IPv4 or
// IPv6 /0 (exit) routes.
func ContainsExitRoute(rr views.Slice[netip.Prefix]) bool {
for i := range rr.Len() {
if rr.At(i).Bits() == 0 {
for _, r := range rr.All() {
if r.Bits() == 0 {
return true
}
}
@@ -205,8 +204,8 @@ func ContainsExitRoute(rr views.Slice[netip.Prefix]) bool {
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
// Routes other than ExitNode Routes.
func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool {
for i := range rr.Len() {
if rr.At(i).Bits() != 0 {
for _, r := range rr.All() {
if r.Bits() != 0 {
return true
}
}

View File

@@ -42,8 +42,8 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if dnsname.HasSuffix(nm.Name, suffix) {
ret[canonMapKey(dnsname.TrimSuffix(nm.Name, suffix))] = ip
}
for i := range addrs.Len() {
if addrs.At(i).Addr().Is4() {
for _, p := range addrs.All() {
if p.Addr().Is4() {
have4 = true
}
}
@@ -52,9 +52,8 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
if p.Name() == "" {
continue
}
for i := range p.Addresses().Len() {
a := p.Addresses().At(i)
ip := a.Addr()
for _, pfx := range p.Addresses().All() {
ip := pfx.Addr()
if ip.Is4() && !have4 {
continue
}

View File

@@ -17,7 +17,7 @@ import (
"sync"
"time"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
)
// These vars are overridden for tests.
@@ -76,21 +76,22 @@ func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
cfg := map[string]string{}
if err := lineread.Reader(r, func(line []byte) error {
for lr := range lineiter.Reader(r) {
line, err := lr.Value()
if err != nil {
return nil, nil, err
}
// accept and skip over empty lines
line = bytes.TrimSpace(line)
if len(line) == 0 {
return nil
continue
}
key, value, ok := strings.Cut(string(line), "=")
if !ok {
return fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
}
cfg[string(key)] = string(value)
return nil
}); err != nil {
return nil, nil, err
}
if cfg["proxy_enabled"] != "yes" {

View File

@@ -213,24 +213,14 @@ type Wrapper struct {
}
type metrics struct {
inboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[dropPacketLabel]
outboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[dropPacketLabel]
inboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
outboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
}
func registerMetrics(reg *usermetric.Registry) *metrics {
return &metrics{
inboundDroppedPacketsTotal: usermetric.NewMultiLabelMapWithRegistry[dropPacketLabel](
reg,
"tailscaled_inbound_dropped_packets_total",
"counter",
"Counts the number of dropped packets received by the node from other peers",
),
outboundDroppedPacketsTotal: usermetric.NewMultiLabelMapWithRegistry[dropPacketLabel](
reg,
"tailscaled_outbound_dropped_packets_total",
"counter",
"Counts the number of packets dropped while being sent to other peers",
),
inboundDroppedPacketsTotal: reg.DroppedPacketsInbound(),
outboundDroppedPacketsTotal: reg.DroppedPacketsOutbound(),
}
}
@@ -886,8 +876,8 @@ func (t *Wrapper) filterPacketOutboundToWireGuard(p *packet.Parsed, pc *peerConf
if filt.RunOut(p, t.filterFlags) != filter.Accept {
metricPacketOutDropFilter.Add(1)
t.metrics.outboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonACL,
t.metrics.outboundDroppedPacketsTotal.Add(usermetric.DropLabels{
Reason: usermetric.ReasonACL,
}, 1)
return filter.Drop, gro
}
@@ -1158,8 +1148,8 @@ func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook ca
if outcome != filter.Accept {
metricPacketInDropFilter.Add(1)
t.metrics.inboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonACL,
t.metrics.inboundDroppedPacketsTotal.Add(usermetric.DropLabels{
Reason: usermetric.ReasonACL,
}, 1)
// Tell them, via TSMP, we're dropping them due to the ACL.
@@ -1239,8 +1229,8 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
t.noteActivity()
_, err := t.tdevWrite(buffs, offset)
if err != nil {
t.metrics.inboundDroppedPacketsTotal.Add(dropPacketLabel{
Reason: DropReasonError,
t.metrics.inboundDroppedPacketsTotal.Add(usermetric.DropLabels{
Reason: usermetric.ReasonError,
}, int64(len(buffs)))
}
return len(buffs), err
@@ -1482,20 +1472,6 @@ var (
metricPacketOutDropSelfDisco = clientmetric.NewCounter("tstun_out_to_wg_drop_self_disco")
)
type DropReason string
const (
DropReasonACL DropReason = "acl"
DropReasonError DropReason = "error"
)
type dropPacketLabel struct {
// Reason indicates what we have done with the packet, and has the following values:
// - acl (rejected packets because of ACL)
// - error (rejected packets because of an error)
Reason DropReason
}
func (t *Wrapper) InstallCaptureHook(cb capture.Callback) {
t.captureHook.Store(cb)
}

View File

@@ -441,13 +441,13 @@ func TestFilter(t *testing.T) {
}
var metricInboundDroppedPacketsACL, metricInboundDroppedPacketsErr, metricOutboundDroppedPacketsACL int64
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}).(*expvar.Int); ok {
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonACL}).(*expvar.Int); ok {
metricInboundDroppedPacketsACL = m.Value()
}
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonError}).(*expvar.Int); ok {
if m, ok := tun.metrics.inboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonError}).(*expvar.Int); ok {
metricInboundDroppedPacketsErr = m.Value()
}
if m, ok := tun.metrics.outboundDroppedPacketsTotal.Get(dropPacketLabel{Reason: DropReasonACL}).(*expvar.Int); ok {
if m, ok := tun.metrics.outboundDroppedPacketsTotal.Get(usermetric.DropLabels{Reason: usermetric.ReasonACL}).(*expvar.Int); ok {
metricOutboundDroppedPacketsACL = m.Value()
}

View File

@@ -71,28 +71,78 @@ package safeweb
import (
"cmp"
"context"
crand "crypto/rand"
"fmt"
"log"
"maps"
"net"
"net/http"
"net/url"
"path"
"slices"
"strings"
"github.com/gorilla/csrf"
)
// The default Content-Security-Policy header.
var defaultCSP = strings.Join([]string{
`default-src 'self'`, // origin is the only valid source for all content types
`script-src 'self'`, // disallow inline javascript
`frame-ancestors 'none'`, // disallow framing of the page
`form-action 'self'`, // disallow form submissions to other origins
`base-uri 'self'`, // disallow base URIs from other origins
`block-all-mixed-content`, // disallow mixed content when serving over HTTPS
`object-src 'self'`, // disallow embedding of resources from other origins
}, "; ")
// CSP is the value of a Content-Security-Policy header. Keys are CSP
// directives (like "default-src") and values are source expressions (like
// "'self'" or "https://tailscale.com"). A nil slice value is allowed for some
// directives like "upgrade-insecure-requests" that don't expect a list of
// source definitions.
type CSP map[string][]string
// DefaultCSP is the recommended CSP to use when not loading resources from
// other domains and not embedding the current website. If you need to tweak
// the CSP, it is recommended to extend DefaultCSP instead of writing your own
// from scratch.
func DefaultCSP() CSP {
return CSP{
"default-src": {"self"}, // origin is the only valid source for all content types
"frame-ancestors": {"none"}, // disallow framing of the page
"form-action": {"self"}, // disallow form submissions to other origins
"base-uri": {"self"}, // disallow base URIs from other origins
// TODO(awly): consider upgrade-insecure-requests in SecureContext
// instead, as this is deprecated.
"block-all-mixed-content": nil, // disallow mixed content when serving over HTTPS
}
}
// Set sets the values for a given directive. Empty values are allowed, if the
// directive doesn't expect any (like "upgrade-insecure-requests").
func (csp CSP) Set(directive string, values ...string) {
csp[directive] = values
}
// Add adds a source expression to an existing directive.
func (csp CSP) Add(directive, value string) {
csp[directive] = append(csp[directive], value)
}
// Del deletes a directive and all its values.
func (csp CSP) Del(directive string) {
delete(csp, directive)
}
func (csp CSP) String() string {
keys := slices.Collect(maps.Keys(csp))
slices.Sort(keys)
var s strings.Builder
for _, k := range keys {
s.WriteString(k)
for _, v := range csp[k] {
// Special values like 'self', 'none', 'unsafe-inline', etc., must
// be quoted. Do it implicitly as a convenience here.
if !strings.Contains(v, ".") && len(v) > 1 && v[0] != '\'' && v[len(v)-1] != '\'' {
v = "'" + v + "'"
}
s.WriteString(" " + v)
}
s.WriteString("; ")
}
return strings.TrimSpace(s.String())
}
// The default Strict-Transport-Security header. This header tells the browser
// to exclusively use HTTPS for all requests to the origin for the next year.
@@ -130,6 +180,9 @@ type Config struct {
// startup.
CSRFSecret []byte
// CSP is the Content-Security-Policy header to return with BrowserMux
// responses.
CSP CSP
// CSPAllowInlineStyles specifies whether to include `style-src:
// unsafe-inline` in the Content-Security-Policy header to permit the use of
// inline CSS.
@@ -168,6 +221,10 @@ func (c *Config) setDefaults() error {
}
}
if c.CSP == nil {
c.CSP = DefaultCSP()
}
return nil
}
@@ -199,16 +256,20 @@ func NewServer(config Config) (*Server, error) {
if config.CookiesSameSiteLax {
sameSite = csrf.SameSiteLaxMode
}
if config.CSPAllowInlineStyles {
if _, ok := config.CSP["style-src"]; ok {
config.CSP.Add("style-src", "unsafe-inline")
} else {
config.CSP.Set("style-src", "self", "unsafe-inline")
}
}
s := &Server{
Config: config,
csp: defaultCSP,
csp: config.CSP.String(),
// only set Secure flag on CSRF cookies if we are in a secure context
// as otherwise the browser will reject the cookie
csrfProtect: csrf.Protect(config.CSRFSecret, csrf.Secure(config.SecureContext), csrf.SameSite(sameSite)),
}
if config.CSPAllowInlineStyles {
s.csp = defaultCSP + `; style-src 'self' 'unsafe-inline'`
}
s.h = cmp.Or(config.HTTPServer, &http.Server{})
if s.h.Handler != nil {
return nil, fmt.Errorf("use safeweb.Config.APIMux and safeweb.Config.BrowserMux instead of http.Server.Handler")
@@ -225,12 +286,27 @@ const (
browserHandler
)
func (h handlerType) String() string {
switch h {
case browserHandler:
return "browser"
case apiHandler:
return "api"
default:
return "unknown"
}
}
// checkHandlerType returns either apiHandler or browserHandler, depending on
// whether apiPattern or browserPattern is more specific (i.e. which pattern
// contains more pathname components). If they are equally specific, it returns
// unknownHandler.
func checkHandlerType(apiPattern, browserPattern string) handlerType {
c := cmp.Compare(strings.Count(path.Clean(apiPattern), "/"), strings.Count(path.Clean(browserPattern), "/"))
apiPattern, browserPattern = path.Clean(apiPattern), path.Clean(browserPattern)
c := cmp.Compare(strings.Count(apiPattern, "/"), strings.Count(browserPattern, "/"))
if apiPattern == "/" || browserPattern == "/" {
c = cmp.Compare(len(apiPattern), len(browserPattern))
}
switch {
case c > 0:
return apiHandler
@@ -341,3 +417,7 @@ func (s *Server) ListenAndServe(addr string) error {
func (s *Server) Close() error {
return s.h.Close()
}
// Shutdown gracefully shuts down the server without interrupting any active
// connections. It has the same semantics as[http.Server.Shutdown].
func (s *Server) Shutdown(ctx context.Context) error { return s.h.Shutdown(ctx) }

View File

@@ -241,18 +241,26 @@ func TestCSRFProtection(t *testing.T) {
func TestContentSecurityPolicyHeader(t *testing.T) {
tests := []struct {
name string
csp CSP
apiRoute bool
wantCSP bool
wantCSP string
}{
{
name: "default routes get CSP headers",
apiRoute: false,
wantCSP: true,
name: "default CSP",
wantCSP: `base-uri 'self'; block-all-mixed-content; default-src 'self'; form-action 'self'; frame-ancestors 'none';`,
},
{
name: "custom CSP",
csp: CSP{
"default-src": {"'self'", "https://tailscale.com"},
"upgrade-insecure-requests": nil,
},
wantCSP: `default-src 'self' https://tailscale.com; upgrade-insecure-requests;`,
},
{
name: "`/api/*` routes do not get CSP headers",
apiRoute: true,
wantCSP: false,
wantCSP: "",
},
}
@@ -265,9 +273,9 @@ func TestContentSecurityPolicyHeader(t *testing.T) {
var s *Server
var err error
if tt.apiRoute {
s, err = NewServer(Config{APIMux: h})
s, err = NewServer(Config{APIMux: h, CSP: tt.csp})
} else {
s, err = NewServer(Config{BrowserMux: h})
s, err = NewServer(Config{BrowserMux: h, CSP: tt.csp})
}
if err != nil {
t.Fatal(err)
@@ -279,8 +287,8 @@ func TestContentSecurityPolicyHeader(t *testing.T) {
s.h.Handler.ServeHTTP(w, req)
resp := w.Result()
if (resp.Header.Get("Content-Security-Policy") == "") == tt.wantCSP {
t.Fatalf("content security policy want: %v; got: %v", tt.wantCSP, resp.Header.Get("Content-Security-Policy"))
if got := resp.Header.Get("Content-Security-Policy"); got != tt.wantCSP {
t.Fatalf("content security policy want: %q; got: %q", tt.wantCSP, got)
}
})
}
@@ -397,7 +405,7 @@ func TestCSPAllowInlineStyles(t *testing.T) {
csp := resp.Header.Get("Content-Security-Policy")
allowsStyles := strings.Contains(csp, "style-src 'self' 'unsafe-inline'")
if allowsStyles != allow {
t.Fatalf("CSP inline styles want: %v; got: %v", allow, allowsStyles)
t.Fatalf("CSP inline styles want: %v, got: %v in %q", allow, allowsStyles, csp)
}
})
}
@@ -527,13 +535,13 @@ func TestGetMoreSpecificPattern(t *testing.T) {
{
desc: "same prefix",
a: "/foo/bar/quux",
b: "/foo/bar/",
b: "/foo/bar/", // path.Clean will strip the trailing slash.
want: apiHandler,
},
{
desc: "almost same prefix, but not a path component",
a: "/goat/sheep/cheese",
b: "/goat/sheepcheese/",
b: "/goat/sheepcheese/", // path.Clean will strip the trailing slash.
want: apiHandler,
},
{
@@ -554,6 +562,12 @@ func TestGetMoreSpecificPattern(t *testing.T) {
b: "///////",
want: unknownHandler,
},
{
desc: "root-level",
a: "/latest",
b: "/", // path.Clean will NOT strip the trailing slash.
want: apiHandler,
},
} {
t.Run(tt.desc, func(t *testing.T) {
got := checkHandlerType(tt.a, tt.b)

View File

@@ -48,7 +48,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/ptr"
"tailscale.com/util/cibuild"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/must"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
@@ -1123,14 +1123,11 @@ func TestSSH(t *testing.T) {
func parseEnv(out []byte) map[string]string {
e := map[string]string{}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
i := bytes.IndexByte(line, '=')
if i == -1 {
return nil
for line := range lineiter.Bytes(out) {
if i := bytes.IndexByte(line, '='); i != -1 {
e[string(line[:i])] = string(line[i+1:])
}
e[string(line[:i])] = string(line[i+1:])
return nil
})
}
return e
}

View File

@@ -6,7 +6,6 @@
package tailssh
import (
"io"
"os"
"os/exec"
"os/user"
@@ -18,7 +17,7 @@ import (
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/osuser"
"tailscale.com/version/distro"
)
@@ -110,15 +109,16 @@ func defaultPathForUser(u *user.User) string {
}
func defaultPathForUserOnNixOS(u *user.User) string {
var path string
lineread.File("/etc/pam/environment", func(lineb []byte) error {
if v := pathFromPAMEnvLine(lineb, u); v != "" {
path = v
return io.EOF // stop iteration
for lr := range lineiter.File("/etc/pam/environment") {
lineb, err := lr.Value()
if err != nil {
return ""
}
return nil
})
return path
if v := pathFromPAMEnvLine(lineb, u); v != "" {
return v
}
}
return ""
}
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {

View File

@@ -149,7 +149,8 @@ type CapabilityVersion int
// - 104: 2024-08-03: SelfNodeV6MasqAddrForThisPeer now works
// - 105: 2024-08-05: Fixed SSH behavior on systems that use busybox (issue #12849)
// - 106: 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
const CurrentCapabilityVersion CapabilityVersion = 106
// - 107: 2024-10-30: add App Connector to conffile (PR #13942)
const CurrentCapabilityVersion CapabilityVersion = 107
type StableID string

View File

@@ -35,7 +35,7 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
cc = "cc"
targetOS = cmp.Or(env.Get("GOOS", ""), nativeGOOS)
targetArch = cmp.Or(env.Get("GOARCH", ""), nativeGOARCH)
buildFlags = []string{"-trimpath"}
buildFlags = []string{}
cgoCflags = []string{"-O3", "-std=gnu11", "-g"}
cgoLdflags []string
ldflags []string
@@ -47,6 +47,10 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
subcommand = argv[1]
}
if subcommand != "test" {
buildFlags = append(buildFlags, "-trimpath")
}
switch subcommand {
case "build", "env", "install", "run", "test", "list":
default:

View File

@@ -163,7 +163,6 @@ GOTOOLCHAIN=local (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "test",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"-race",

View File

@@ -126,6 +126,7 @@ type Server struct {
initOnce sync.Once
initErr error
lb *ipnlocal.LocalBackend
sys *tsd.System
netstack *netstack.Impl
netMon *netmon.Monitor
rootPath string // the state directory
@@ -432,8 +433,7 @@ func (s *Server) TailscaleIPs() (ip4, ip6 netip.Addr) {
return
}
addrs := nm.GetAddresses()
for i := range addrs.Len() {
addr := addrs.At(i)
for _, addr := range addrs.All() {
ip := addr.Addr()
if ip.Is6() {
ip6 = ip
@@ -518,6 +518,7 @@ func (s *Server) start() (reterr error) {
}
sys := new(tsd.System)
s.sys = sys
if err := s.startLogger(&closePool, sys.HealthTracker(), tsLogf); err != nil {
return err
}
@@ -546,7 +547,7 @@ func (s *Server) start() (reterr error) {
sys.HealthTracker().SetMetricsRegistry(sys.UserMetricsRegistry())
// TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how?
ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
ns, err := netstack.Create(tsLogf, sys.Tun.Get(), eng, sys.MagicSock.Get(), s.dialer, sys.DNSManager.Get(), sys.ProxyMapper())
if err != nil {
return fmt.Errorf("netstack.Create: %w", err)
}
@@ -1227,6 +1228,13 @@ func (s *Server) CapturePcap(ctx context.Context, pcapFile string) error {
return nil
}
// Sys returns a handle to the Tailscale subsystems of this node.
//
// This is not a stable API, nor are the APIs of the returned subsystems.
func (s *Server) Sys() *tsd.System {
return s.sys
}
type listenKey struct {
network string
host netip.Addr // or zero value for unspecified

View File

@@ -1080,13 +1080,6 @@ func TestUserMetrics(t *testing.T) {
t.Errorf("metrics1, tailscaled_health_messages: got %v, want %v", got, want)
}
// The node is the primary subnet router for 2 routes:
// - 192.0.2.0/24
// - 192.0.5.1/32
if got, want := parsedMetrics1["tailscaled_primary_routes"], wantRoutes; got != want {
t.Errorf("metrics1, tailscaled_primary_routes: got %v, want %v", got, want)
}
// Verify that the amount of data recorded in bytes is higher or equal to the
// 10 megabytes sent.
inboundBytes1 := parsedMetrics1[`tailscaled_inbound_bytes_total{path="direct_ipv4"}`]
@@ -1131,11 +1124,6 @@ func TestUserMetrics(t *testing.T) {
t.Errorf("metrics2, tailscaled_health_messages: got %v, want %v", got, want)
}
// The node is the primary subnet router for 0 routes
if got, want := parsedMetrics2["tailscaled_primary_routes"], 0.0; got != want {
t.Errorf("metrics2, tailscaled_primary_routes: got %v, want %v", got, want)
}
// Verify that the amount of data recorded in bytes is higher or equal than the
// 10 megabytes sent.
outboundBytes2 := parsedMetrics2[`tailscaled_outbound_bytes_total{path="direct_ipv4"}`]

View File

@@ -13,14 +13,19 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"testing"
"tailscale.com/util/set"
)
type DepChecker struct {
GOOS string // optional
GOARCH string // optional
BadDeps map[string]string // package => why
GOOS string // optional
GOARCH string // optional
BadDeps map[string]string // package => why
WantDeps set.Set[string] // packages expected
Tags string // comma-separated
}
func (c DepChecker) Check(t *testing.T) {
@@ -29,7 +34,7 @@ func (c DepChecker) Check(t *testing.T) {
t.Skip("skipping dep tests on windows hosts")
}
t.Helper()
cmd := exec.Command("go", "list", "-json", ".")
cmd := exec.Command("go", "list", "-json", "-tags="+c.Tags, ".")
var extraEnv []string
if c.GOOS != "" {
extraEnv = append(extraEnv, "GOOS="+c.GOOS)
@@ -54,6 +59,11 @@ func (c DepChecker) Check(t *testing.T) {
t.Errorf("package %q is not allowed as a dependency (env: %q); reason: %s", dep, extraEnv, why)
}
}
for dep := range c.WantDeps {
if !slices.Contains(res.Deps, dep) {
t.Errorf("expected package %q to be a dependency (env: %q)", dep, extraEnv)
}
}
t.Logf("got %d dependencies", len(res.Deps))
}

View File

@@ -26,7 +26,7 @@ import (
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
@@ -288,7 +288,7 @@ func (s *Server) serveNoiseUpgrade(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
noisePrivate := s.noisePrivKey
s.mu.Unlock()
cc, err := controlhttp.AcceptHTTP(ctx, w, r, noisePrivate, nil)
cc, err := controlhttpserver.AcceptHTTP(ctx, w, r, noisePrivate, nil)
if err != nil {
log.Printf("AcceptHTTP: %v", err)
return

View File

@@ -10,6 +10,7 @@ import (
"net/netip"
"os"
"slices"
"time"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcapgo"
@@ -279,10 +280,28 @@ type Network struct {
svcs set.Set[NetworkService]
latency time.Duration // latency applied to interface writes
lossRate float64 // chance of packet loss (0.0 to 1.0)
// ...
err error // carried error
}
// SetLatency sets the simulated network latency for this network.
func (n *Network) SetLatency(d time.Duration) {
n.latency = d
}
// SetPacketLoss sets the packet loss rate for this network 0.0 (no loss) to 1.0 (total loss).
func (n *Network) SetPacketLoss(rate float64) {
if rate < 0 {
rate = 0
} else if rate > 1 {
rate = 1
}
n.lossRate = rate
}
// SetBlackholedIPv4 sets whether the network should blackhole all IPv4 traffic
// out to the Internet. (DHCP etc continues to work on the LAN.)
func (n *Network) SetBlackholedIPv4(v bool) {
@@ -361,6 +380,8 @@ func (s *Server) initFromConfig(c *Config) error {
wanIP4: conf.wanIP4,
lanIP4: conf.lanIP4,
breakWAN4: conf.breakWAN4,
latency: conf.latency,
lossRate: conf.lossRate,
nodesByIP4: map[netip.Addr]*node{},
nodesByMAC: map[MAC]*node{},
logf: logger.WithPrefix(s.logf, fmt.Sprintf("[net-%v] ", conf.mac)),

View File

@@ -3,7 +3,10 @@
package vnet
import "testing"
import (
"testing"
"time"
)
func TestConfig(t *testing.T) {
tests := []struct {
@@ -18,6 +21,16 @@ func TestConfig(t *testing.T) {
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", HardNAT))
},
},
{
name: "latency-and-loss",
setup: func(c *Config) {
n1 := c.AddNetwork("2.1.1.1", "192.168.1.1/24", EasyNAT, NATPMP)
n1.SetLatency(time.Second)
n1.SetPacketLoss(0.1)
c.AddNode(n1)
c.AddNode(c.AddNetwork("2.2.2.2", "10.2.0.1/16", HardNAT))
},
},
{
name: "indirect",
setup: func(c *Config) {

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