Commit Graph
523 Commits
Author SHA1 Message Date
Brad Fitzpatrick faa10ddaaa go.toolchain.branch: switch to Go 1.27
Promote the toolchain from Go 1.26.6 to Go 1.27.0, matching what
go.toolchain.next.rev has been testing. Besides the toolchain files
themselves (updated by pull-toolchain.sh), this bumps the go.mod go
directive, the Dockerfile golang base image, and the README, and
regenerates the depaware.txt files and the gzip assets in
tempfork/spf13/cobra and util/eventbus, whose bytes change with
Go 1.27's rewritten compress/flate.

Also bump golangci-lint to v2.13.1, the first release line built
with Go 1.27; the prebuilt v2.10.1 binary refuses to target a Go
version newer than the one it was built with.

Also bump golang.org/x/net to v0.58.0 (plus the sibling x/ module
upgrades it requires) to pick up upstream commit 8d10596d2624
(http2: avoid deadlocks in wrapped ClientConn state callback),
which we hit during Go 1.27 rc testing.

Also add docs/go-bump-checklist.md for next time.

Updates #20220

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia3e4c9effafbc91227eed39efb52f1fba1b8d89c
2026-08-27 09:52:15 -07:00
BeckyPauley 37317073ba cmd/k8s-operator/e2e: support real clusters and registries (#20911)
Extend e2e test setup to work with a remote registry and real cluster.
Previously only kind was supported.

Detect cluster node architecture and build images only for the given
architecture to reduce build time. Build for all platforms as the fallback
option. This assumes only one node architecture per cluster, which is
reasonable for our test cases.

By default, --build loads the built images into a kind cluster. Use with
--registry to instead specify and push to a remote registry.

Fixes tailscale/corp#46577

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-08-27 07:46:59 +01:00
chaosinthecrd 1de85e1972 cmd/k8s-operator: watch ProxyGroups for HA Services with a Service handler
The service-pg-reconciler reconciles Services annotated for an ingress
ProxyGroup, but its ProxyGroup watch reused ingressProxyGroupFilter,
which is ingressesFromIngressProxyGroup. That handler lists Ingresses
and returns Ingress keys, so when a ProxyGroup became Available the
requests it produced never matched a Service and the reconciler's Get
just came back NotFound.

This fixes this by adding servicesFromIngressProxyGroup, which lists the
Services indexed for the ProxyGroup and returns their keys, matching what
the egress path already does with egressSvcsFromEgressProxyGroup.

Fixes #20944

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-21 18:19:37 +01:00
chaosinthecrd 0ed6af7b72 cmd/k8s-operator: return the requeue result from HA Service reconcile
Fixes a dropped assignment of `res` in HA service reconciler necessary
for requeuing.

Fixes #20946
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-21 16:46:12 +01:00
joshrzemien c2f3230c80 k8s-operator: allow annotationless resources under proxy group policy (#20907)
Guard optional annotations maps in generated CEL expressions before
looking up expose or proxy-group annotations. This lets ordinary
annotationless Services and Ingresses pass admission while preserving
deny-all and allowlist behavior for present proxy-group annotations.

RELNOTE: Kubernetes ProxyGroupPolicy now permits annotationless resources.

Fixes #20906

Change-Id: I8b9475d34c003ca6d233246c021ac656e0530fb1

Signed-off-by: joshrzemien <joshua.k.rzemien@gmail.com>
2026-08-20 12:27:18 +01:00
Will HannahandWill Hannah e1ffeeffba client,feature/favorites: add locally-pinned favorites (#20563)
Let clients pin favorite devices, exit nodes, and services so GUIs can
surface & change them. Pins are stored per login profile in the new
favorites feature module, keyed per category; devices and exit nodes by
StableNodeID, services by ServiceName.

The item types live in a leaf package feature/favorites/pintype,
keeping them out of the core ipn hierarchy. Each category has
its own type (pintype.Device, pintype.ExitNode, pintype.Service).

Exposed over LocalAPI at GET/POST /localapi/v0/pins, where POST replaces
only the categories named in the request so a client can update one
category without clobbering the others. Pins are local to the device and
are not synced across a user's devices.

updates tailscale/corp#44836

Signed-off-by: Will Hannah <willh@tailscale.com>
Co-authored-by: Will Hannah <wph@Wills-Virtual-Machine.local>
2026-08-19 09:52:56 -04:00
BeckyPauley 083e8ddfc2 cmd/k8s-operator/e2e: add tests to cover DNSConfig (#20861)
Deploy k8s nameserver during e2e test setup, and point the cluster resolver
(CoreDNS or kube-dns) to it so tests can resolve MagicDNS names inside the
cluster.

Add a test to verify singleton L7 Ingress is reachable from inside the
cluster using its MagicDNS Name.

Update existing egress tests to use a dedicated tailnet target per test (to
avoid conflicts). Egress tests now also verify that an egress target is
reachable from within the cluster using both its Service and MagicDNS name.

To successfully curl using the target's MagicDNS name, publish test CAs as a
ConfigMap to the cluster, and mount these for each curl pod.

Fixes tailscale/corp#38027

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-08-17 14:22:14 +01:00
James Tucker 0d66cde14f tstest/membudget: add live-heap budget regression tests
Static size checks (iossize) catch binary dirty-page growth but nothing
covered runtime heap cost, which is what actually consumes the iOS
Network Extension's 50 MiB jetsam budget. Bring up a tsnet backend
(with the full condregister feature set, matching shipping clients)
against an in-process testcontrol server and assert live post-GC heap
budgets for (a) backend startup with zero peers and (b) marginal cost
per netmap peer.

The startup test measures 1.3 MiB today and fails loudly on the
conn25 flow-table pre-allocation regression (17 MiB) that jetsam-killed
the iOS extension on large tailnets.

Budgets are deliberately generous (6-12x current measurements) to stay
flake-free while still catching the multi-MiB regressions that matter
for mobile.

A new debugknob enables us to constrain the GSO/GRO batch size to 1 for
these tests so as to avoid the memory allocation associated with those
buffers, which are a known issue with their own work stream.

Updates tailscale/corp#46408
Updates tailscale/corp#18514

Signed-off-by: James Tucker <james@tailscale.com>
2026-08-14 16:45:04 -07:00
David Bond 5e7f189e04 cmd/k8s-operator: move dnsrecords and nameserver into their own packages (#19696)
This commit moves the reconcilers for both the DNS nameserver and
DNSConfig custom resource into their own packages within
`k8s-operator/reconciler`

Closes: https://github.com/tailscale/corp/issues/37088

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-08-12 19:55:16 +01:00
David Bond 13a293a563 cmd/k8s-operator: ensure CRDs are always generated
This commit modifies the "generate" tool we use on the kubernetes operator
that produces helm chart and static manifest assets for CRDs.

Previously, this required always remembering to add new constants to
a `main.go` and did not have any mechanism to fail in CI if you forgot
to. Now this tool will iterate over all the CRDs and ensures that they're
in the places they're expected to be, with a test that will fail if they
are not.

This removes the requirement for remembering to add these constants
every time you have a new CRD.

Closes: #20594
Signed-off-by: David Bond <davidsbond93@gmail.com>
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-12 15:55:08 +01:00
David Bond 7f458941ae k8s-operator: make PeerRelay endpoints reachable on EKS by default (#20834)
This commit changes how PeerRelay services are exposed on AWS. A Network
Load Balancer only forwards to targets in an availability zone enabled
on it, and spec.aws.elasticIPs pins each service to a single subnet,
which enables just one zone. A replica scheduled anywhere else silently
receives nothing while still reporting PeerRelayReady with an address in
status.endpoints.

Without spec.aws we now leave the subnet unpinned, so the AWS Load
Balancer Controller spreads the load balancer over every zone it finds,
and cross-zone load balancing is on by default so any of its addresses
reach the pod. Hostname resolution is no longer gated on the
eip-allocations annotation, which had left these unpinned services in
EndpointsPending forever, and a failure to resolve now logs at debug
since it is expected while a load balancer provisions.

Such a load balancer has an address per zone, and AWS bills for each, so
every one of them is now advertised rather than only the lowest sorted.
That also lets a peer reach the relay when one zone is unreachable.
status.endpoints gains address as a second list map key so a replica can
hold an entry per address; no field changes, so existing readers of
endpoints[].address keep working. Readiness counts the replicas that
have an endpoint rather than the entries, so a replica with several
addresses cannot mask one that has none.

The pods now serve containerboot's health check endpoint and the load
balancer is pointed at it over HTTP. A peer relay listens only on UDP,
so the default TCP check against the port the load balancer forwards
could never succeed and every target sat unhealthy while relaying
perfectly well. /healthz reports 200 once the device has tailnet
addresses, which is the condition that actually matters.

The CRD docs now describe spec.aws as the exception, note that it also
needs a ProxyClass pinning pods to the zone of the subnets it names, and
drop the claim that an Elastic IP has an availability zone of its own.

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

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-08-12 15:31:16 +01:00
chaosinthecrd 6a6742c90e cmd/k8s-operator: attach egress readiness gate to ProxyGroup Pods
The egressPodsReconciler (added in #14792) only sets the
tailscale.com/egress-services readiness condition on egress ProxyGroup
replica Pods that declare the corresponding readiness gate. However, the
gate was never actually added to the egress Pod template, so the
reconciler always hit its early-return and the readiness condition was
never set. This commit adds said readiness gate to the egress Pod template.

Updates #14326

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-12 14:58:17 +01:00
chaosinthecrd 0e93fdea5a cmd/k8s-operator,k8s-operator/reconciler: dedupe auth key reissuance
ProxyGroup, Recorder, and PeerRelay each carried a near-identical copy of
the auth key re-issuance state machine (in-flight tracking, per-parent rate
limiting, stale-device cleanup). A bug fix had to land in three places and
could silently drift.

Extract it into a shared tailscaled.Reissuer, alongside the other tailscaled
workload helpers (NewAuthKey, AuthKeyFromConfigSecret, DeviceIDFromStateSecret)
that the callers already use. It owns its own mutex, tracks in-flight reissues
per replica keyed by parent, and rate-limits re-issuance per parent; the three
reconcilers drive it via EnsureState/RemoveState/ShouldReissue. The device
deletion helper is shared too, so the reissue state machine and its tests now
live in one place.

Updates #20544

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-12 14:57:28 +01:00
Simon Law 00699abdfb tailcfg,tailcfg/{nodecap,selfcap}: split capability constants to their own packages (#20639)
Package tailcfg defines the types and constants used by the Tailscale
protocol, but since everything is all in one package, it’s difficult
to sift through the docs: https://pkg.go.dev/tailscale.com/tailcfg

We define and enumerate capabilities as string constants for
tailcfg.NodeCapability and tailcfg.PeerCapability. This PR extracts
them into their own packages:

- tailcfg.CapabilityFileSharing becomes nodecap.FileSharing
- tailcfg.NodeAttrOnlyTCP443 becomes nodecap.OnlyTCP443
- tailcfg.PeerCapabilityTaildrive becomes peercap.Taildrive

We originally intended for CapabilityFoo to grant an entitlement or
permission for Foo, and for NodeAttrBar to configure Bar in the
nodeAttrs section of the policy file. However, there was no technical
enforcement of this convention, so new capabilities have used the
NodeAttr prefix regardless of meaning. Therefore, this PR unifies
tailcfg.CapabilityFoo and tailcfg.NodeAttrBar into a single package as
nodecap.Foo and nodecap.Bar.

Ran `go fix -inline ./...` and committed the changes that replaced
uses of the tailcfg aliases with the authoritative ones.

Updates #20259

Change-Id: Ieb7e7e6c8247c39faf42fdf15c68cdc7c621c730
Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-08-07 16:30:35 -07:00
David Bond 80522b814c cmd/k8s-operator: enable IP forwarding without the sysctl binary (#20768)
The sysctler init container shells out to sysctl to turn on IP
forwarding for non-userspace proxies. That binary ships in the
procps-ng package, which Alpine has but Red Hat's UBI does not, so
on UBI the init container exits 127 and every proxy Pod is stuck in
PodInitializing and never registers a device.

Updates: https://github.com/tailscale/corp/issues/45981
Updates: https://github.com/tailscale/corp/issues/44443

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-08-07 16:59:06 +01:00
David Bond 13b5f3c5f3 cmd/k8s-operator: allow specifying base image in e2e tests (#20727)
This commit adds a new `--base-image` flag to the e2e test suite
so that tests can build the operator and operator accessories
with a different base docker image. We want this so that we can
try things out with red hat's UBI as part of getting the operator
up and running on openshift clusters.

We can then modify the e2e test runner to use a matrix for normal
alpine base and redhat's.

Updates: https://github.com/tailscale/corp/issues/45981
Updates: https://github.com/tailscale/corp/issues/44443

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-08-05 18:41:15 +01:00
chaosinthecrd 21a3f6413a cmd/k8s-operator/deploy/chart: support nameOverride and fullnameOverride
The Helm chart hardcoded the operator's Deployment, ServiceAccount,
Role, and RoleBinding names to "operator".

Adds a standard _helpers.tpl with name/fullname template functions and
use them for the operator's resources, including the oauth Secret and
the cluster-scoped ClusterRole/ClusterRoleBinding. When neither
nameOverride nor fullnameOverride is set, the fullname resolves to the
historical hardcoded names rather than the release name, so existing
installations upgrade with no resource renames; rendering with default
values is byte-identical to before.

Fixes tailscale/tailscale#18232

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-04 19:08:48 +01:00
chaosinthecrd 37aca4aa2e cmd/k8s-operator: support imagePullSecrets on DNSConfig nameserver
The nameserver Deployment created for a DNSConfig was the only
operator-managed workload with no way to configure imagePullSecrets, so its
pods could not pull the nameserver image from a private registry.

Adds imagePullSecrets to NameserverPod and thread it through to the nameserver
Deployment pod spec.

Updates #16772

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-04 17:14:01 +01:00
chaosinthecrd 15b03cf5bc cmd/k8s-operator: set imagePullSecrets on proxy ServiceAccounts
The Helm chart only applied imagePullSecrets to the operator Deployment's
pod spec, so proxy pods (which use a different ServiceAccount) never
received them and could not pull images from private registries.

Apply imagePullSecrets to the operator, proxies, and kube-apiserver-auth-proxy
ServiceAccounts.

Updates #16772

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-04 17:14:01 +01:00
David Bond ca79c1e09b cmd/k8s-operator,k8s-operator/reconciler: add PeerRelay e2e test (#20663)
This commit adds a new end-to-end test for the `PeerRelay` custom
resource.

This test is currently quite limited due to the fact that we are running
our tests within a kind cluster within github actions. This means it's
not really possible to give the peer relays a proper public IP address
via the `LoadBalancer` type services that we spin up.

That being said, we intend to expand our e2e test suite with actual
real clusters in future so this can be expanded upon at a later date.

For now, this test spins up a single and multi-replica deployment of
a peer relay and confirms that it has been registered with control
and is configured to act as a peer relay.

This test also caught a small bug where the server URL was not being
passed into the peer relay's configuration, which has been fixed here.

Closes: https://github.com/tailscale/corp/issues/45731
Closes: https://github.com/tailscale/corp/issues/45737

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-08-04 14:29:03 +01:00
chaosinthecrd 33042fb97b go.mod: bump sigs.k8s.io/controller-runtime to v0.23.3
PR #20388 (commit ca9f6971e) bumped k8s.io/* from v0.34 to v0.35 as a
transitive effect of pulling in helm v3.21 (for containerd CVE fixes).
The old controller-runtime v0.19.4 is aligned with k8s 1.31 APIs and
regresses at runtime against v0.35 client-go (informer reflectors stall
on Watch responses), leaving the operator unable to reconcile.

v0.23.3 is the release aligned with k8s.io/* v0.35 / Kubernetes 1.35.
The operator uses a narrow slice of controller-runtime (manager +
builder + client + reconcile + handler + source), so this bump is a
lift-and-shift: no reconciler wiring changes needed.

Test scaffolding fix: controller-runtime v0.20+ populates TypeMeta
(Kind/APIVersion) on objects returned by the fake client. The shared
expectEqual helper now strips TypeMeta before diffing so existing
tests continue to work without needing every 'want' object updated.

Tested: deployed to live clusters across Kubernetes 1.33, 1.34, 1.35,
and 1.36 (identical build per cluster). Verified the operator starts and
reconciles cleanly on all versions with no RBAC/forbidden errors, CRDs
establish and enforce validation, and the field indexers resolve
correctly under annotation churn (no stale-index misrouting). Exercised
the Service, ProxyClass, Connector, egress (simple + ProxyGroup, incl.
the EndpointSlice reconciler), and HA ingress-for-pg reconcilers.
Confirmed informer/watch recovery after an API server restart forces a
re-list/re-watch (the exact regression this bump fixes).

Updates tailscale/corp#44898

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-07-31 18:36:38 +01:00
David Bond eaf1b56ffe cmd/k8s-operator: move proxyclass reconciler into its own package (#19695)
This commit moves the proxyclass reconciliation logic from its original
home in the rather large main package of the k8s-operator to a dedicated
package within `k8s-operator/reconciler/proxyclass`.

Closes: https://github.com/tailscale/corp/issues/37085

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-07-30 11:37:54 +01:00
Simon Law bb498a8393 net/netcheck: extract Compare function for region latencies (#20658)
There are two places in the code where we need to compare the
latencies of a netcheck report. In both cases, the comparison wasn’t
well tested.

This PR extracts that logic into a Compare method of the new
RegionLatency type. This type wraps the map of latency measurements
keyed by region ID.

Updates #cleanup

Change-Id: I7f248988973007c2f452283c1b82f84b03068f77

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-29 16:51:55 -04:00
BeckyPauley d4dce80927 cmd/k8s-operator/e2e: use Let's Encrypt staging environment (#20666)
When running k8s operator e2e tests against real tailnets, use Let's
Encrypt's staging environment to avoid production rate limits.

Updates tailscale/corp#45571

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-07-29 15:15:45 +01:00
David Bond 7dec0c7d7a cmd/k8s-operator: add e2e tests for Connector resources (#20650)
This commit adds 3 end-to-end tests to the suite for the
operator's `Connector` resources. Connectors are used
to deploy subnet routers, exit nodes & app connectors
within a Kubernetes cluster.

One test exists for each respective mode of the resource
and assertions are made against control that the machine
is running in the expected mode using the device identifier
within the pod's state secret.

Closes: https://github.com/tailscale/corp/issues/38025

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-07-29 11:37:50 +01:00
BeckyPauley 122ed8a44f cmd/k8s-operator/e2e: add egress tests (#20648)
Add end-to-end tests for egress to a tailnet target via its FQDN, IPv4 and
IPv6 addresses.

Update the kind cluster used in tests to support dual-stack (to allow
testing egress to both IPv4 and IPv6 tailnet targets). For pre-existing
clusters, detect on test setup which IP families the cluster supports.
IPv4/IPv6 subtests are skipped if not supported.

Run the end-to-end tests in parallel to improve the speed of test runs. To
enable this, run TestProxyGroupPolicy in a dedicated namespace so its
ValidatingAdmissionPolicies don't affect other parallel tests sharing the
same namespace.

Updates tailscale/corp#34834

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-07-28 18:21:44 +01:00
chaosinthecrd 97a75c837d cmd/k8s-operator,ipn/store/kubestore,kube/kubetypes: share ACME account key per tailnet
Introduce a per-tailnet shared ACME account key so that all ingress
ProxyGroup replicas on a tailnet present the same account identity to
Let's Encrypt. This lets renewals claim the ARI "replaces" exemption
from the 50-certs-per-week rate limit, surviving Pod restarts,
ProxyGroup recreation, and cluster migrations.

The operator provisions a "tailscale-acme-accounts" Secret in its
namespace, guarded by a finalizer and a deletion warning event, and
watched so it is recreated promptly if removed. Proxies migrate any
pre-existing per-pod key into the shared Secret on first boot, adopt
the shared key on subsequent boots, and restore it on cert writes if
the Secret was recreated empty. Certs are stamped with the fingerprint
of the issuing account so renewals skip the "replaces" claim when the
account doesn't match.

Opt-in per-ProxyGroup via the tailscale.com/share-acme-account
annotation, or operator-wide via OPERATOR_SHARED_ACME_ACCOUNT_KEY.

Updates #18251
Updates #20288

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-07-27 12:06:41 +01:00
David Bond 9535e3b99b cmd/k8s-operator: include peer relay CRD in generation (#20593)
This commit modifies the generation command for the kubernetes operator
to include the CRD for peer relays in the helm chart and static
manifests

Updates: #fixup

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-07-24 14:01:41 +01:00
Rollie Ma f4978b4b9b feature,client: add serviceclientprefs for desktop client service launch (#20501)
Add serviceclientprefs, an optional feature that stores and loads the
desktop clients' saved service launch preferences, one file per login
profile.

- Add GET|POST /localapi/v0/prefs/service-clients to load and save the
  current profile's service client prefs.
- Add local client GetServiceClientPrefs and SetServiceClientPref that
  call the new local api endpoint.
- Store the prefs with the ipn/store FileStore at
  TailscaleVarRoot()/profile-data/<profileID>/service-client-prefs/<hex-encoded-key>,
  so DeleteProfile cleans them up for free. Fall back to an in-memory
  store when there's no var root.
- Register the feature and its local api route from build tagged files
  so the whole thing drops out under ts_omit_serviceclientprefs.
- Add the serviceclient package holding Pref and Prefs (saved client,
  username, database name, and last used time), so the local api client
  and desktop apps can import the types without the feature machinery.

Change-Id: I340a99c1b332d181fb1556fbf3e8003bb3b95a08
Updates: https://github.com/tailscale/tailscale/issues/20429

Signed-off-by: Rollie Ma <rollie@tailscale.com>
2026-07-22 20:36:05 -07:00
David Bond be0e460a20 cmd/k8s-operator,k8s-operator: Kubernetes Peer Relays (#20495)
This commit contains the Kubernetes implementation of peer relays via the new `PeerRelay` CRD. It's a mega branch consisting of the commits of other PRs gone into this work:

1. https://github.com/tailscale/tailscale/pull/20211
2. https://github.com/tailscale/tailscale/pull/20329
3. https://github.com/tailscale/tailscale/pull/20423
4. https://github.com/tailscale/tailscale/pull/20503

An instance of the `PeerRelay` CRD deploys a `StatefulSet` of containerboot instances configured to advertise themselves as peer relays using the IP addresses configured via `LoadBalancer` services on each cloud provider (with some AWS specifics as it's less automatic than its competing cloud providers). 

Per replica, a `LoadBalancer` type `Service` resource is provisioned and its IP address is used to configure the respective relay.

This has been tested with success in AWS, GCP & Azure and provides additional modification to `Service` resources via the CRD for any other kinds of deployment environments. It also contains some work that may appear to be duplication of what already exists within `cmd/k8s-operator` so we can start building an appropriate migration path for `Connector`, `ProxyGroup` etc into respective `k8s-operator/reconciler/*` packages.

Closes https://github.com/tailscale/corp/issues/34524
2026-07-20 16:37:15 +01:00
Brad Fitzpatrick 82cfea90ca all: fix JSON serialization under Go 1.27's finalized encoding/json/v2
Go 1.27 enables GOEXPERIMENT=jsonv2 by default: encoding/json is now
backed by the json/v2 machinery, and github.com/go-json-experiment/json
compiles as a thin alias of the standard library's encoding/json/v2.
Several tag options and behaviors we relied on did not make the cut for
the final Go 1.27 API, breaking tailscaled at runtime and four packages'
tests. This change adapts to the final API while keeping the wire format
byte-for-byte identical on all Go versions.

First, the `format` tag option was demoted to experimental. Its mere
presence in a struct tag now makes marshaling and unmarshaling fail at
runtime. tailcfg.SSHAction.SessionDuration had `format:nano` (added in
a2dc517d7 to pin the v1 representation), so on Go 1.27 any netmap
containing an SSH policy failed to decode, breaking every PollNetMap.
Remove the option here and in net/speedtest; time.Duration still
marshals as int64 nanoseconds under encoding/json on all Go versions
(Go 1.27's v1 mode sets FormatDurationAsNano by default), so old
clients and servers are unaffected. Add a regression test locking in
the exact wire format.

Consequently, invert the cmd/vet jsontags rule: it previously required
an explicit `format` tag on time.Duration fields, which is now exactly
wrong. It now rejects any `format` tag option, which would have caught
this bug in CI.

Second, the `inline` tag option was renamed to `embed`. The standard
library silently ignores `inline`, while the pinned go-json-experiment
module (used on Go 1.26) only knows `inline`. Specify both options in
types/prefs and logtail; each implementation ignores the option it does
not know, producing identical output. Drop `inline` once we require
Go 1.27.

Third, encoding/json (v1) now dispatches to MarshalJSONTo and
UnmarshalJSONFrom methods and its v1 options flow into nested
jsonv2.MarshalEncode calls. Types whose v1 methods deliberately
routed through jsonv2 for v2 semantics (types/opt.Value, the
types/prefs preference types) would silently change wire format
(e.g. nil slices becoming null). Pin jsonv2.DefaultOptionsV2 in
their jsonv2 methods so the representation is the same regardless
of the entry point.

Finally, json.Marshal costs one more allocation under Go 1.27,
tripping the types/logger.AsJSON alloc test. Switch its fmt.Formatter
to jsonv2.MarshalWrite with explicit v1 options, which writes directly
to the fmt.State: one allocation on both toolchains with unchanged
output. Depaware files pick up the go-json-experiment/json/v1 options
shim as a new dependency of types/logger.

With this change, go test ./... passes with both Go 1.26.5 and
go1.27rc2.

Updates #20220
Fixes #20528
Fixes #20254

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I694c7d57fd81e55a579c579e9be10032bca569d4
2026-07-19 09:58:50 -07:00
Brad Fitzpatrick 4660a961eb ipn/ipnlocal, wgengine/wgcfg/nmcfg: stop building peer lists on delta path
Processing a peer add/remove delta still materialized the full netmap
(an O(n) slicesx.MapValues plus sort over all peers, at 10k+
peers in a large tailnet) twice per delta: once in UpdateNetmapDelta
purely to hand the self node to Engine.SetSelfNode, and once in
authReconfigLocked.

Neither needs peers anymore. SetSelfNode gets the self node from the
existing nodeBackend.Self accessor. authReconfigLocked only reads
self-node fields (SelfNode, NodeKey, GetAddresses, HasCap) now that
WireGuard peers ride the incremental route manager and per-peer config
source, so it can use the peers-free NetMap accessor.

That also makes nmcfg.WGCfg vestigial: since wgcfg.Config lost its
Peers field, its peer walk existed only to emit the [v1] skip logs
(expired peers, unselected exit nodes, unaccepted subnet routes),
duplicating filtering the route manager already does. Delete the
package and construct the two-field wgcfg.Config inline. The skip
logs go away; if they're missed, the route manager can log them
incrementally at upsert time instead of rescanning every peer on
every reconfig.

With this, the runtime.DidRange analysis (see the ts_rangehook test)
shows a delta netmap update performing no O(n) range loops except
updateRouteManagerExtras, and the delta phase of that test drops from
1.09s to 0.14s for 400 deltas at n=10000 (from 4.79s at the
start of this effort, before the incremental route manager work).

Updates #12542

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia0e03ef9db0c988790b2c29de1f0505305e93f58
2026-07-15 11:21:36 -04:00
Brad Fitzpatrick bb4f458207 feature/captiveportal: move captive portal code out of ipnlocal, netcheck
Captive portal detection was half-migrated: it had a build tag and
buildfeatures constant, but its code still lived in build-tag-gated
files in ipn/ipnlocal and net/netcheck, with its per-backend state
(context, cancel func, signaling channel) as fields on LocalBackend.

Move it under feature/captiveportal. The health-driven detection loop
becomes an ipnext.Extension holding its own state: it starts and
stops the loop from the BackendStateChange hook and subscribes to
health.Change events on the eventbus itself, removing the captive
portal hooks and special cases from LocalBackend entirely. The DERP
map now comes from a new ipnext.NodeBackend.DERPMap method, and the
preferred DERP region from magicsock's last netcheck report (the
same underlying source as the previously used Hostinfo.NetInfo).

The netcheck probe hook is now exported with a signature free of
netcheck internals, and its implementation moves to the small
feature/captiveportal/netcheckhook package, which installs the hook
as an import side effect. That package stays free of tsd/wgengine
dependencies so the tailscale CLI can keep probing for captive
portals in "tailscale netcheck" without linking the daemon-side
extension. The net/captivedetection library itself is unchanged and
stays put; after this change it is only linked when something pulls
in netcheckhook or the feature extension.

tailscaled links the feature by default via condregister as before,
but tsnet no longer does (shrinking tsnet, k8s-operator, and tsidp);
tsnet users who want it can blank-import the feature package, and
tsnet's dep test now locks that in.

Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3f1d09f9dc03e18f9a648ab5e42d16fa540b3fa9
2026-07-14 20:22:23 -04:00
Brad Fitzpatrick f0ce89b715 net/tstun,wgengine,ipn/ipnlocal: make tstun's peerConfigTable use RouteManager table
Previously tstun.Wrapper.SetWGConfig walked wgcfg.Config.Peers on every
netmap to rebuild its own IP-to-peer table for masquerade NAT rewrites
and jailed-peer classification. Now the tun layer instead consumes the
route manager's shared immutable outbound snapshot directly, via a new
Engine.SetPeerRoutes method: LocalBackend pushes the snapshot (plus this
node's native Tailscale addresses) after every route manager commit that
can change it, and per-packet lookups read the interned PeerRoute
attributes from that table.

When no current peer is jailed or masqueraded, LocalBackend installs a
nil table (gated on RouteManager.HasDataPlaneAttrs), preserving the
per-packet nil-check fast path. The exitNodeRequiresMasq machinery is
deleted: its purpose was populating the table with all peers so that
more-specific entries shadow an exit node's /0, and the always-full
route manager table gives that shadowing inherently.

This is the last step before removing the Peers field from wgcfg.Config.

Updates #12542

Change-Id: Ifce09ca929a3f2511303ca1d6efdd583739494ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-14 12:41:11 -07:00
Mike Jensen 9bd62683dd go.mod: revert update vulnerable dependencies (#20435) (#20456)
This reverts commit 468a7f4973 on request to @ChaosInTheCRD

Although passing all our CI checks, @ChaosInTheCRD would like to plan manual testing as part of incorporating these updates.

Updates #cleanup

Change-Id: I3f007b571b884c9538a97ac5d3ded782bcba2347

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-07-14 10:26:32 -06:00
Mike Jensen 468a7f4973 go.mod: update vulnerable dependencies (#20435)
This change updates vulnerable dependencies with a direct fix path. Updated:
  * github.com/prometheus/prometheus@v0.311.3 - Direct dependency addressing https://pkg.go.dev/vuln/GO-2026-5710 and https://pkg.go.dev/vuln/GO-2026-5662
  * github.com/go-openapi/swag@v0.27.0 - Needed to fix mutual dependency on github.com/go-openapi/testify after prometheus update
  * github.com/go-git/go-git/v5@v5.19.1 - Addresses https://pkg.go.dev/vuln/GO-2026-5496
  * helm.sh/helm/v3@v3.21.1 - Root update to address most containerd CVEs
  * github.com/containerd/containerd@v1.7.33 - Addresses remaining container CVEs, in total: https://pkg.go.dev/vuln/GO-2026-5758 https://pkg.go.dev/vuln/GO-2026-5475 https://pkg.go.dev/vuln/GO-2026-5378
  * sigs.k8s.io/controller-runtime updated to v0.23.3 - This is needed to accommodate the k8s.io/api v0.35.3 update (test change needed for update)

Vulnerabilities were discovered from govulncheck, which includes reachability in the analysis.

Updates #cleanup

Change-Id: I8345745d22a7e6ee106b58c410889e0aef748be4

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-07-14 08:29:50 -06:00
BeckyPauley 6ee7bcb458 cmd/{k8s-operator,containerboot,kube}: support IPv6 in egress ProxyGroup (#19898)
* cmd/{k8s-operator,containerboot,k8s-proxy},kube: support IPv6 in egress ProxyGroup

  Add support for dual-stack and IPv6 clusters in egress ProxyGroup.
  Previously, egress ProxyGroup only supported IPv4: the operator and
  containerboot assumed IPv4 for ClusterIP Services, EndpointSlices,
  and health check headers.

  This change introduces the following:

  - Create a per-family EndpointSlice instead of a single IPv4
    EndpointSlice.

  - Update the egress services readiness reconciler to account for
    both IPv4 and IPv6 EndpointSlices.

  - Update the pod readiness reconciler to use the primary Pod IP
    (PodIPs[0]) for readiness checks, instead of hard-coding to use
    IPv4.

  - Update the /healthz handler to return both PodIPv4Header and
    PodIPv6Header.

  - Add an IPv6 address field to egress status.

  - Update containerboot and k8s-proxy to use the new health check
    logic.

Updates tailscale/corp#41677

Change-Id: If66a3146df48c75b1e65a71632bbc9fc75feded2
Signed-off-by: Becky Pauley <becky@tailscale.com>

* cmd/{k8s-operator,containerboot}: improve dual-stack egress ProxyGroup

On dual-stack clusters, an egress ProxyGroup Service has one EndpointSlice
per IP family (IPv4 and IPv6). However, EndpointSlices were only recreated
when the ExternalName Service configuration changed, so a deleted
EndpointSlice was not recreated. The egress readiness reconciler also had
no mechanism to identify which IP families should exist (previously only
an IPv4 EndpoitSlice was required).

We now create an EndpointSlice for every IP family the ClusterIP Service
supports.

Also mark an egress Service NotReady when an EndpointSlice for an expected IP
family (derived from the Service's ClusterIPs) is missing, so a
dual-stack Service missing a family's EndpointSlice is no longer reported
Ready.

Clarify that the egress pre-shutdown and Pod readiness health checks
verify only one IP family on dual-stack clusters.

Change-Id: I35b03daf76ac817cd516e9a731770b2d85f6ee16
Signed-off-by: Becky Pauley <becky@tailscale.com>

---------

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-07-14 13:26:40 +01:00
Tom Meadows 236564af75 cmd/k8s-operator: reorder Ingress cleanup so cert loop stops before VIPService delete (#20426)
The cert loop only stops when the domain leaves the ServeConfig.
Deleting the VIPService first left the loop hammering ACME for a
domain the control plane no longer recognised, burning retry slots.

Reorder to: remove from serve config, unadvertise, delete VIPService,
clean cert resources.

Updates #20288

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-07-14 12:11:56 +01:00
Brad Fitzpatrick ff1c7ef23c ipn/ipnlocal,net/routemanager: keep a routemanager.RouteManager updated per node
Give nodeBackend a RouteManager and keep it in sync as routing
inputs change: full netmaps resync the whole peer set (removals plus
no-op-cheap upserts), incremental netmap deltas mirror their peer
upserts and removes into the same mutation batch, and
authReconfigLocked pushes the routing-relevant prefs (exit node,
subnet route acceptance, OneCGNAT) after resolving the exit node's
stable ID to its current numeric node ID.

A selected exit node that doesn't resolve to a current peer (a
nonexistent node, or MDM's "auto:any" placeholder awaiting
resolution) is not the same as no exit node: per the long-standing
ipn.Prefs.ExitNodeID contract, it blackholes internet traffic rather
than letting it escape to the local network. RouteManager's Prefs
gains an ExitNodeSelected bit so its OS route set keeps the default
routes in that case, with no outbound peer to carry them, matching
what routerConfigLocked does today, as pinned by TestRouterConfigExitNodeBlackhole in the previous commit.

All mutations happen with nodeBackend.mu held, satisfying the
RouteManager's serialized Begin/Commit contract.

Nothing consumes its snapshots yet; the wgengine data plane and OS
router wiring come next.

Updates #12542

Change-Id: I677b6b2c9efb8e41b3d27071bd9db73e01640d3b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-13 10:20:54 -07:00
Fernando Serboncini 505330d09f Revert "go.mod: Update vulnerable dependencies (#20388)" (#20420)
This reverts commit ca9f6971e5.

The dependency updates broke the K8s E2E tests. Reverting so the
updates can be re-landed with the tests passing.

flake.nix, shell.nix, and flakehashes.json were regenerated with
tool/updateflakes rather than reverted, since a later commit
(6fdffd9e5) also updated them for the gowebdav bump.

Change-Id: Id4afd7788d305a674841168e2a66a0009212ffd3

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
2026-07-13 10:56:05 -04:00
Adriano Sela Aviles 66a51c426f cmd: apply go fix
Updates #cleanup

Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
2026-07-10 14:26:11 -07:00
Mike Jensen ca9f6971e5 go.mod: Update vulnerable dependencies (#20388)
This change updates vulnerable dependencies with a direct fix path. Updated:
  * github.com/prometheus/prometheus@v0.311.3 - Direct dependency addressing https://pkg.go.dev/vuln/GO-2026-5710 and https://pkg.go.dev/vuln/GO-2026-5662
  * github.com/go-openapi/swag@v0.27.0 - Needed to fix mutal dependency on github.com/go-openapi/testify after prometheus update
  * github.com/go-git/go-git/v5@v5.19.1 - Addresses https://pkg.go.dev/vuln/GO-2026-5496
  * helm.sh/helm/v3@v3.21.1 - Root update to address most containerd CVEs
  * github.com/containerd/containerd@v1.7.33 - Addresses remaining container CVEs, in total: https://pkg.go.dev/vuln/GO-2026-5758 https://pkg.go.dev/vuln/GO-2026-5475 https://pkg.go.dev/vuln/GO-2026-5378

Updates #cleanup

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-07-09 15:52:34 -06:00
Brad Fitzpatrick 692f84df8d wgengine,wgcfg,feature/netlog: move network flow logging behind a feature hook
wgcfg.Config.NetworkLogging carried the network flow logging identity
inside the WireGuard config, where it was unrelated to WireGuard; it
lived there mainly so that identity changes would defeat Reconfig's
ErrNoChanges check and reach the netlog startup/shutdown logic.

Remove the field and move the whole netlog lifecycle into a new
feature/netlog package, installed on the engine via the new
wgengine.HookNewNetLogger hook, like other feature/* packages. The
logging identity now comes from LocalBackend's current netmap via the
widened NetLogSource interface (replacing Engine.SetNetLogNodeSource),
so nmcfg no longer parses audit log IDs into the config. The engine
still calls the hook before its ErrNoChanges return and before
router.Set (to capture initial packets), and again after router.Set
(to capture final packets), preserving the previous ordering.

Core wgengine no longer imports wgengine/netlog, so minimal builds
drop it entirely. tailscaled keeps netlog via feature/condregister,
and tsnet imports feature/condregister/netlog explicitly to keep
netlog enabled by default in tsnet-based binaries (tsidp,
k8s-operator).

This is pulled out of a future change that removes wgcfg.Config.Peers,
to make that PR smaller.

Updates #12542
Updates #12614

Change-Id: I41ca7dfe43c51e977c41b5f8e934bd1f0e6e6e24
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-09 12:56:37 -07:00
BeckyPauley 384e776dfa cmd/k8s-operator: ensure EndpointSlices exist on every egress reconcile (#20347)
EndpointSlices were created in provision(), which was called only if certain
fields on the ExternalName Service had changed. If an EndpointSlice was
deleted, it was never re-created (because the owning Service had not
changed).

Move EndpointSlice provisioning after this gated provision step so that it
runs on every reconcile.

Fixes #20322

Change-Id: I416fb5e4b40f2029efb97aa6ca7ceb3e31b0d52d

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-07-08 16:39:20 +01:00
Alex Chan 3d52c3f03e all: fix more typos caused by unnecessary repetition
Updates #cleanup

Change-Id: I5c0b8f0152581231252ab97dd1820d8b3fcbe450
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-07-06 10:36:17 +01:00
Simon Law 932260511e ipn/ipnlocal: use routecheck reports to make exit node suggestions
Now that the routecheck subsystem is continuously collecting
reachability reports in the background, we can add a hook to
LocalBackend for fetching its report. That allows
suggestExitNodeUsingTrafficSteering to consult that report when
disqualifying candidates, instead of blocking on an immediate probe.

Exit node suggestions will only consult the report when the
`client-side-reachability` and `client-side-reachability-routecheck`
node attributes are both set on the current node.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-02 20:26:27 -07:00
Brad Fitzpatrick 1c77079fd7 ipn/ipnlocal, feature/acme: move most remaining cert code into feature/acme
f5eac39ea ("feature/acme, ipn/ipnlocal: start moving ACME/cert state
into an extension") started to move the cert code into feature/acme
but was meant as a baby step.

This goes further, moving almost everything, leaving only some hooks
in ipnlocal.

When we later move "serve" support out to feature/serve, this will
look a bit different in that the hooks currently in ipnlocal will move
to feature/serve (cert support already depends on serve).

As part of this, cert-related tests move to feaure/acme too, which
means some test infra from ipnlocal now moves to shared ipnlocaltest.
(it's not big at the moment, but I imagine it growing)

Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9ea89aa9754f12d54b81751b6bd830f2664241ff
2026-06-29 12:57:22 -07:00
Brad Fitzpatrick 5ebc7497ea tsnet: link in feature/acme by default
This was missing in the earlier f5eac39ea7 and meant that tsnet users weren't
getting (all of) acme support.

Thanks to @ChaosInTheCRD and @BeckyPauley for debugging.

Updates #12614
Updates #20252

Change-Id: I176a7b179b2ad3726aca484057f0aae7cc3561c8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-06-29 09:04:57 -07:00
Alex Freestone af999f05cf k8s-operator/dnsrecords: fix dnsRR dropping reconcile events on lock err (#19968)
On optimistic lock error, requeue the event after a short duration.

Resolves a case where a failure to acquire an optimistic lock on the
dnsrecords configmap will cause the operator to drop a reconcile event
and leave the configmap in an undesirable state.

Updates tailscale/tailscale#19946

Signed-off-by: Alex Freestone <freestone.alex@gmail.com>
2026-06-26 13:52:05 +01:00
Brad Fitzpatrick aefb1531d1 net/tsdial, ipn/ipnlocal: stop using netmap.NetworkMap in Dialer
tsdial.Dialer.SetNetMap rebuilt an O(n peers) map of MagicDNS names on
every netmap change. As we move toward per-peer incremental deltas,
this becomes quadratic. This removes it and replaces it with
SetResolveMagicDNS, a callback into LocalBackend that looks up
hostnames from nodeBackend's new nodeByName index (populated alongside
nodeByAddr/nodeByKey on both full and delta paths). The index stores
both FQDNs and short names as keys.

This is the same treatment applied to netlog (8f210454d), wglog
(988b0905b), and drive (1d6989408): stop pushing *netmap.NetworkMap
into subsystems and instead have them pull from LocalBackend's live
data via callbacks.

Updates #12542

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I24557ab0c8a27636e08e4779bcfd3ec633db0a78
2026-06-24 13:14:45 -07:00