Commit Graph
11154 Commits
Author SHA1 Message Date
Joe Tsai f2dd208f53 util/bufpool: add a general purpose pool of buffers
A naive sync.Pool that stores dynamically sized []byte
is incorrect since the Pool assumes that each stored item
carries approximately the same memory cost.

Different cost items should be stored in different pools.
The bufpool tracks buffer pools by powers of two
and provides a Buffer type that mimics the API of bytes.Buffer.

This is a performance optimization. Avoid using this unless
there is a clear performance gain for doing so.
Incorrect use can lead to widespread data corruption
(as is already the case with any incorrect sync.Pool use).

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2026-08-14 11:51:24 -07:00
Adrian Dewhurst 5a1066f494 tailcfg/peercap, types/appctype: prepare for conn25 grants
This change contains the protocol changes needed to support
describing authorization for conn25 apps. As a temporary transition
measure during development, app configurations can disable authorization
enforcement.

Updates tailscale/corp#40076

Change-Id: I3183c10374aacb0048f6632c384f71f758c20f2f
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2026-08-14 13:48:46 -04:00
Michael Ben-Ami 0953fd9a97 conn25: return early in handleConnectorTransitIP if conn25 not configured
We were checking and writing an HTTP error, but not returning.

Updates tailscale/corp#39033

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-08-14 07:31:34 -04:00
Patrick O'Doherty 4782f36506 go.mod: update to Go 1.26.6 (#20857) 2026-08-13 21:11:13 -07:00
Jordan Whited ab0489912f go.mod,wgengine/wgcfg: bump wireguard-go for device.Option
Move away from mutating global, exported variables in wireguard-go. Use
device.Option's passed to device.NewDevice, instead. No functional
changes, just API cleanup in preparation of future changes.

Updates tailscale/corp#22467
Updates tailscale/corp#46396
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-08-13 15:05:36 -07:00
Joe Tsai 9f4fe8b5f2 util/ioqueue: new log buffer implementation (#20816)
This adds a new ring buffer implementation that aims to replace
logtail.Buffer and the on-disk implementation in filch.Filch.

There are several problems with filch.Filch:

* Filching stderr should not be done at the buffer layer.
  This makes structured representation within the buffer difficult
  as arbitrary stderr data may unexpectedly appear,
  which hinders attempts at more structured data.

* Log messages are assumed to be discreet lines rather than arbitrary bytes.
  This makes it harder to switch the structured representation (e.g., using CBOR instead).

* Data that appears asynchronously through stderr never triggers a wake-up within logtail.
  Consequently logs may never be uploaded.

* Relatedly, there is no mechanism for notifying that data has newly arrived in the buffer.

* There is no two-stage exfiltration. The TryReadLine method may or may not persist
  the fact that the data was read. It arbitrarily depends on whether we cross
  a magical file boundary in the dual-file approach.
  A failed upload followed by a restart results in dropped logs.
  A successful upload followed by a restart results in duplicated logs.

The new Buffer interface and VolatileBuffer implementation are
a step in the direction to resolving these problems.

* In the future, filching will output to a separate pipe
  that we explicitly process the data for,
  before putting it into the log buffer.
  By processing the data, we can protect against stderr garbage being inserted
  into the buffer unexpectedly breaking any structure.

* The Buffer.Peek and Buffer.DiscardUntil methods provide a way
  to exfiltrate in a two-step manner.
  When uploading, we peek at a chunk of data to upload.
  When successful, we discard the data, ensuring that the buffer knows
  not to provide that data again. The Len method can be used to suggest
  to the logging service the amount of back pressure that exists.

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2026-08-13 13:04:53 -07:00
Aaron Klotz 4bf9382c3b net/dns: only attempt registry changes on address families that are actually enabled on interface
We stop waiting to see if registry keys come available; this causes bad
interactions with the LocalBackend watchdog.

Instead we check the network interface for availability of AF_INET,
AF_INET6, and AF_NETBIOS. If no IP families are available on the
interface, we fail with an error. Otherwise we only attempt to configure
DNS for the address families that are actually enabled.

We also avoid changing any NetBIOS settings if AF_NETBIOS is disabled.

Fixes #46276

Change-Id: Ic7ae8a3dc810f4c428085f8b3ad905c6c32ae351
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2026-08-13 13:13:20 -06:00
Michael Renner ea9b1a2309 tka: avoid quadratic filesystem scans
Since #17567, FS.ChildAUMs scans and decodes every active AUM file.
Authority reconstruction and compaction call it repeatedly, making
filesystem work quadratic in the number of AUMs.

Build per-FS indexes of active AUM hashes and parent-to-child
relationships in one scan. Use the indexes for graph queries while
continuing to decode returned AUM values from disk. Invalidate the
indexes after mutations, but retain them for empty purges.

A production-sized 825-AUM benchmark improves from 124 seconds to
4 seconds on macOS with Defender and from 5.9 seconds to 34 milliseconds
on Linux ext3. The checked-in benchmark exercises the 2,000-AUM maximum
linear history accepted by the traversal guardrail.

RELNOTE: Avoid Tailnet Lock startup failures with large authority histories.

Fixes #20735

Change-Id: I088136bc2e9d1b6bfdecb223c069d42400c9f63d
Signed-off-by: Michael Renner <terrorobe@github.com>
2026-08-13 11:33:45 +01:00
Amal Bansode fd07b9a2b6 net/traffic: fix rendezvous hashing (#20828)
The rendezvous hasher for traffic steering loadbalancing was flawed.
By plainly using the FNV-1a hash value, the result often reflected the
magnitude of the most significant bits in the hash seed, meaning the
hash function was not diffusive (aka missing the Avalanche Effect).

Popular wisdom seems to be that the output of FNV-1a should be mixed
with some large numbers to perturb more output bits. Borrow concepts
from other (Rust, Java) libraries by using the mix13 variant of 64-bit
finalizers by David Stafford.

Modify the fuzz test that asserts this fairness. Adjust a few
constants like client count and candidate count to more closely
reflect real-world scenarios and practical probabilities. Tighten
the bounds for distribution from 50% to +-20%.

Updates tailscale/corp#46471

Signed-off-by: Amal Bansode <amal@tailscale.com>
2026-08-12 14:13:42 -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
M. J. Fromberger 93912bbe1d ipn/ipnlocal/netmapcache: invalidate digest caches when removing objects (#20839)
Previously, a delta update that drops a peer would not invalidate the digest
cache for that peer after deleting its cache entry. If a subsequent (later)
delta re-adds that peer with the same content, the digest cache would prevent
us from updating the persistent entry.  Add a test to exercise this, and fix
the bug.

Updates #20795

Change-Id: I4a9ef03e8787a330d6395629251ee61ca2221fdd
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-08-12 09:49:18 -07: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 db07a34204 k8s-operator/reconciler/peerrelay: add missing tsclient import
authkey.go references tsclient.Client in getAuthKey but never imported
the package, breaking main.

Updates #20544

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-08-12 15:24:51 +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
James Tucker d200b3f18f feature/conn25: don't pre-size flow table maps
NewFlowTable pre-sized its two per-direction lookup maps to maxEntries.
The datapath handler creates a client table (10k flows) and a connector
table (100k flows) unconditionally at extension init, so every client
paid ~15.8 MiB of empty map buckets up front (measured on device, and
reproducible with a benchmark: 2x100k + 2x10k maps keyed by the 38-byte
flowtrack.Tuple).

On iOS the Network Extension has a hard 50 MiB jetsam limit. On large
tailnets the netmap and derived engine state need ~18 MiB of live heap
on top of the baseline, and this pre-allocation pushed the process over
the limit: the extension was killed (per-process-limit) seconds after
connecting, in a relaunch loop, making such tailnets unusable on iOS.

maxEntries is still enforced as a bound at insertion time; the maps now
grow on demand instead. On an iPhone 14 Pro Max joining a ~600 node
tailnet this took peak live heap during netmap ingest from 36.8 MiB to
24.3 MiB and the extension now connects and stays up with ~18 MiB of
headroom instead of being killed at 50 MiB.

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

Signed-off-by: James Tucker <james@tailscale.com>
2026-08-11 15:52:08 +02:00
Brad Fitzpatrick 69efc99bd2 version: use -g hash prefix in unstamped builds of other repos
The unstamped Long() fallback (plain go build/install, no linker
stamps) always prefixed the buildvcs commit hash with "t". But that
hash describes the main module's repo, which for binaries built from a
repo that imports tailscale.com (e.g. Tailscale's proprietary repo) is
not the tailscale.com repo. That made e.g.
"1.103.0-dev20260811-t4bb67392c" ambiguous with the stamped scheme,
where "t" always means the tailscale.com commit and "g" the commit of
the repo the binary was built from.

Keep "t" when the main module is tailscale.com itself, and use "g"
otherwise, matching mkversion's meaning of the two prefixes.

Updates tailscale/corp#44945

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I42ed6dce6572854ead487b20b0889460c47d0f58
2026-08-11 14:43:02 +02:00
Joe Tsai d2c5166298 util/cobs: add new package for frame encoding (#20371)
Package cobs implements Consistent Overhead Byte Stuffing (COBS),
a technique for reliable packet framing over serial byte streams.

This has future utility for storing a sequence of arbitrary log entries
on disk without needing to depend on intrinsic framing within
the log entries themselves (e.g., JSON or CBOR).

While more complicated, COBS is superior to offset-based framing
mechanisms as the null byte can be trivially used to demarcate
the boundaries of a frame. This makes COBS more resistant
against bit-corruption where a single corrupted offset
can make everything else in the file unreadable.
COBS makes it possible to resynchronize framing after a
corrupted section by simply searching for the next null.

Performance:

	Benchmark/EncodeForward/Zeros-32         	   16341	     76312 ns/op	13740.68 MB/s	       0 B/op	       0 allocs/op
	Benchmark/EncodeReverse/Zeros-32         	    6326	    188261 ns/op	5569.79 MB/s	       0 B/op	       0 allocs/op
	Benchmark/DecodeForward/Zeros-32         	   16461	     72140 ns/op	14535.28 MB/s	       0 B/op	       0 allocs/op

	Benchmark/EncodeForward/NonZeros-32      	   41797	     29155 ns/op	35965.56 MB/s	       0 B/op	       0 allocs/op
	Benchmark/EncodeReverse/NonZeros-32      	    4792	    248788 ns/op	4214.74 MB/s	       0 B/op	       0 allocs/op
	Benchmark/DecodeForward/NonZeros-32      	   35790	     34584 ns/op	30319.92 MB/s	       0 B/op	       0 allocs/op

	Benchmark/EncodeForward/Random-32        	   23042	     53727 ns/op	19516.64 MB/s	       0 B/op	       0 allocs/op
	Benchmark/EncodeReverse/Random-32        	    3164	    374590 ns/op	2799.26 MB/s	       0 B/op	       0 allocs/op
	Benchmark/DecodeForward/Random-32        	   27241	     58506 ns/op	17922.41 MB/s	       0 B/op	       0 allocs/op

EncodeReverse performance is notably slower than EncodeForward
because modern CPU architectures are not as optimized for
reading from memory in reverse.
However, reverse encoding is necessary if appending into
a dst buffer that is identical to the src buffer.
In such a case, the CPU performance hit is worth the benefit
of avoiding an intermediate allocation.
Speeds of GB/s is still plenty fast enough and
magnitudes faster than JSON or CBOR encoding.

Updates #17242
Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2026-08-11 01:52:25 -07:00
Jordan Whited dabc50d0fe go.mod: bump wireguard-go
Updates tailscale/corp#45906
Updates tailscale/corp#45803

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-08-10 20:25:03 -07:00
Brendan Creane efae57a58c tstest/natlab: test that a peer's name doesn't shadow a bare upstream name (#20804)
* tstest/natlab: test that a peer's name doesn't shadow a bare upstream name

With MagicDNS off, only suffixed names should be answered locally, but
since 1.102 quad-100 also answers a bare, unqualified name whenever a
tailnet device shares that name. It returns the device's Tailscale IP
instead of forwarding to the tailnet's global nameserver, leaving the
upstream record unreachable.

TestBareNameNotHijackedByPeer configures a global nameserver owning a
single-label name, adds a peer named to collide with it, and asserts the
client's lookup returns the upstream address. It queries via "tailscale
dns query" so the name stays a single label; a search domain completing
it would resolve a different name and pass regardless.

Add SplitDNSBareName to vnet's split-DNS zone to serve that name, and a
packet-level case asserting the fake server answers it.

Updates #20789

Signed-off-by: Brendan Creane <bcreane@gmail.com>

* tstest/natlab: check short names resolve via quad-100 when MagicDNS is on

TestMagicDNS asserted a peer's short name resolves, but through getent
with a search domain configured: libc completed it to the FQDN, so the
lookup never asked quad-100 for a single label. The bare-name path the
resolver takes when MagicDNS is enabled was untested.

Query the short name with "tailscale dns query" too, which asks for
exactly the name given. This is the enabled-MagicDNS mirror of
TestBareNameNotHijackedByPeer, and pins the other side of the condition
added in 1ec348784: a fix that dropped short names unconditionally,
rather than only when MagicDNS is disabled, now fails here.

Updates #20789

Signed-off-by: Brendan Creane <bcreane@gmail.com>

---------

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-08-10 18:23:44 -07:00
Nick Khyl 1ec348784b ipn/ipnlocal,net/dns/resolver: prevent bare name resolution when MagicDNS is disabled
nodeBackend.nodeByName should always contain both FQDNs and short names,
as it is used in different contexts, including UserDial DNS resolution, which should
be able to resolve unqualified DNS names regardless of the MagicDNS state.

However, net/dns/resolver.Resolver and, by extension, MagicDNSHosts
implementations should only resolve fully qualified domain names,
skipping short names when MagicDNS is disabled for the tailnet.

This fixes it in (*nodeBackend).nodeByFQDNLocked, which is only used
in the MagicDNS paths, and updates the tests.

Fixes #20789

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-08-10 17:54:32 -05:00
Brad Fitzpatrick 25877455e7 misc/git_hook/githook: don't flag large blobs already on the remote
The pre-push large-blob check diffed the pushed tree only against the
remote's old tree for the same ref. After rebasing a stale branch past
an unrelated large-file change on the default branch, that diff shows
the large file as changed even though the exact blob is already on the
remote via main, rejecting the push with a false positive.

Diff against every available base tree instead: the remote's old
commit for the ref plus the merge base with the remote's default
branch. Only flag a file that is a large addition relative to all
bases, so blobs the remote already has are not reported, while
genuinely new large files on the branch are still rejected.

Updates tailscale/corp#9863

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia0c98fc1f5ab67a2913f948aeff605c72641ada7
2026-08-10 12:00:07 +02:00
Brad Fitzpatrick 8be8ff5eec cmd/containerboot: use synctest in TestRefreshAdvertiseServices
This test was the repo's slowest at 60 seconds of wall time, all of it
spent sleeping: three of its subtests reach the unconditional 20 second
failover wait in kube/services.EnsureServicesAdvertised, despite using
a pure in-memory FakeLocalClient with no real control or I/O.

Run each subtest in a testing/synctest bubble so the wait elapses on
the fake clock instead. The test now completes in milliseconds.

Fixes #20792

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3442815f7efcf6de740f893197ee0461ab049bb2
2026-08-10 11:19:33 +02:00
Brendan Creane e1e5325c22 net/dns/resolver: reach netstack-only upstreams over UDP (#20786)
sendTCP dials through tsdial.Dialer and so honors UseNetstackForIP, but
sendUDP opened a host-stack socket via packetListener and never consulted
the dialer. In userspace networking mode (tsnet, or tailscaled
--tun=userspace-networking) there is no tun device, so a split-DNS query
to a tailnet resolver blackholed for the full udpRaceTimeout before the
TCP fallback answered it.

Add dialUDP, which picks between the netstack dialer and the existing
packetListener the same way tsdial.Dialer.dialOneUser does, and adapt the
connected netstack conn to nettype.PacketConn. sendUDP is otherwise
unchanged, so txid checks, SERVFAIL/REFUSED handling, TC flagging and EDNS
clamping are identical on both paths.

Unskips the UDP subtest of TestForwarderNetstackUpstream, which now
answers in ~300µs rather than 2s, and adds unit tests for the dispatch and
for truncation over the netstack path. TestSplitDNSToTailnetResolverUDP
covers the whole path end to end over real gVisor: two tsnet nodes with no
tun, one resolving a split-DNS name whose upstream is the other.

Fixes #20314

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-08-09 09:01:45 -07:00
M. J. Fromberger e592a0c363 staticcheck.conf: enable ST1000 to check package docs (#20787)
Fix the small number of existing violations of this check, and enable it for
future runs. The fixes needed were:

 - Clean up a few misspelled package names (probably renames).
 - Clean up a few lexical nits ("Package x" instead of "The x package").
 - Add lint directives to some files affected by build tag variance.
 - Add a missing package comment and re-generate the k8s docs.

The lint overrides are a little ugly, but there are only a few places where we
need them, and it's probably worthwhile to enable the check on the rest of the
repo. Rather than replicate the docs around the build tag, I made the lint
diagnotics reference the "correct" file.

Updates #cleanup

Change-Id: I0d97f2f468542af456a0396cf9a023f04f23e436
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-08-08 13:02:06 -07:00
Brendan Creane 8bebdca90c net/dns/resolver: add test for upstream resolvers reached via netstack (#20779)
sendTCP dials through tsdial.Dialer and so honors UseNetstackForIP, but
sendUDP always uses a host-stack socket. In userspace networking mode
there is no route to the tailnet, so a split-DNS query to a tailnet
resolver only succeeds after falling back to TCP. The two subtests
differ only in transport, isolating that gap; the UDP one is skipped
pending a fix.

Updates #20314

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-08-08 08:26:38 -07:00
Joe Tsai fa4bb7eae0 types/jsonformat: add wrapper types for JSON custom formats
The json/v2 prototype used to support a `format` tag option,
which has been removed for the initial release of json/v2 in Go 1.27.

The wrapper types in this package provide a way to avoid using
the `format` tag option for all existing use-cases.

The types are written to cooperate with other tag options
such as `string`, which may stringify JSON numbers.
We adjust cmd/vet/jsontags accordingly.

Updates #20220
Updates tailscale/corp#45953

Change-Id: Ie1fcea41dc30983e9acc43085f42a6e8ee49d26e
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2026-08-08 13:38:20 +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
Will HannahandJonathan Nobels 15015f19bf net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed (#20775)
* net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed

On sandboxed macOS, an uncovered control ExtraRecord forced quad-100 to
be the primary resolver, proxying all public DNS and shadowing a user's
DoH system profile. Scope quad-100 to its match domains instead, adding
the uncovered host records to MatchDomains so they still resolve while
public names fall through to the OS resolver. quad-100 remains primary
only without a usable base resolver or with non-enumerable MagicDNS
host records.

fixes tailscale/corp#45534

Signed-off-by: Will Hannah <willh@tailscale.com>

* net/dns: move scoped DNS behind an envknob

updates tailscale/corp#45534

Given the sensitivity of this change, let's stuff it behind
a control knob for a release.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>

---------

Signed-off-by: Will Hannah <willh@tailscale.com>
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
Co-authored-by: Jonathan Nobels <jonathan@tailscale.com>
2026-08-07 15:27:22 -04: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
Claus Lensbøl 9dbcc9c2e0 client/systray: add missing exec bit on autostart folder (#20772)
Updates #cleanup

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-08-07 08:32:53 -04:00
Brendan CreaneandClaude Opus 5 a265908a72 tstest/natlab: add a second fake DNS server and split-DNS tests (#20553)
Add a second fake DNS server (4.11.4.12) serving a zone the default one
doesn't, so a split-DNS route can be verified: resolving that name proves the
query was forwarded there.

Tailscale implements a split-DNS route two ways, and a test covers each. When
the OS resolver supports split DNS and every route shares one resolver set,
tailscaled hands the OS that resolver directly and quad-100 stays out of the
query path; TestSplitDNSOSForwarded covers this, reaching the split resolver
over a subnet route as a real deployment would. Otherwise the OS is pointed at
quad-100, which forwards per-domain itself; TestSplitDNS covers this against
both the systemd-resolved and direct backends, along with the other ways a
lookup resolves -- answered locally by quad-100 from an extra record or from the
netmap, and unrouted names still going to the machine's normal resolver.

TestSplitDNSNoMagicDNS covers split DNS on a tailnet with MagicDNS off, which
the two above leave out: one has mixed resolver sets but MagicDNS on, the other
MagicDNS off but a single resolver set. Together those conditions take a
distinct path through compileConfig, where tailscaled reads the OS base
resolver config rather than scoping quad-100 to a match domain.

Both arrangements produce the same answers, so each test also asserts the
guest's resolver state to prove which one it exercised.

Fixes tailscale/corp#44798

Signed-off-by: Brendan Creane <bcreane@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:05:51 -07:00
Brendan Creane 44ec3a1b89 Revert "net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed (#20603)" (#20771)
This reverts commit 7e01825e51.

The change was merged to main accidentally. Reverting so it can go
back through review before landing again.

updates tailscale/corp#45534

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-08-06 16:33:23 -07:00
Will HannahandJonathan Nobels 7e01825e51 net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed (#20603)
* net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed

On sandboxed macOS, an uncovered control ExtraRecord forced quad-100 to
be the primary resolver, proxying all public DNS and shadowing a user's
DoH system profile. Scope quad-100 to its match domains instead, adding
the uncovered host records to MatchDomains so they still resolve while
public names fall through to the OS resolver. quad-100 remains primary
only without a usable base resolver or with non-enumerable MagicDNS
host records.

fixes tailscale/corp#45534

Signed-off-by: Will Hannah <willh@tailscale.com>

* net/dns: move scoped DNS behind an envknob

updates tailscale/corp#45534

Given the sensitivity of this change, let's stuff it behind
a control knob for a release.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>

---------

Signed-off-by: Will Hannah <willh@tailscale.com>
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
Co-authored-by: Jonathan Nobels <jonathan@tailscale.com>
2026-08-06 13:01:16 -07:00
Fran Bull deded79f0e feature/conn25: handle type HTTPS DNS responses
Parse the response and filter out IPV4Hint and IPV6Hint.

Fixes tailscale/corp#46099

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-08-05 12:17:57 -07: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
Evan Lowry 2b8a980f1f ipn/ipnlocal: deduplicate RoutableIPs / RequestTags in hostinfo (#20759)
Both RoutableIPs and RequestTags act as a set of values. There have been
cases of misconfigured IAC populating duplicate values, resulting in
more data being sent to control than needs to be.

Updates tailscale/corp#44607

Signed-off-by: Evan Lowry <evan@tailscale.com>
2026-08-05 12:28:47 -03:00
Adrian Dewhurst 616dcd5378 feature/conn25: add app name to DoH query string
To prepare for enforcing permission to access an app, it is extremely
helpful if the client reports which app it expects the query is for,
instead of making the connector check all possible matching apps.

Updates tailscale/corp#40076

Change-Id: Ib41e0af4c0134d06dc4acda97a39c0a3adb88968
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2026-08-04 14:43:31 -04: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
Brad Fitzpatrick 0dfe672b32 ipn/ipnlocal: allow the ingress peer capability for unsigned peers (#20745)
0eb38dc2e (#20561) made peer capability resolution return nothing for
peers with UnsignedPeerAPIOnly set, so that a possibly malicious
control server can't grant capabilities to peers outside the tailnet
lock authority. But Tailscale Funnel ingress nodes are unsigned by
design, and control intentionally grants them
PeerCapabilityIngress, which the peerapi /v0/ingress handler requires.
The result was that every Funnel connection was rejected with a 403
"denied; no ingress cap".

Instead of denying all capabilities to unsigned peers, allowlist
PeerCapabilityIngress specifically. It only permits ingress requests
over the PeerAPI, which unsigned peers can already reach, and the
node only serves them for targets explicitly configured for Funnel.

The tsnet TestFunnel didn't catch the regression because its fake
ingress peer was a normal signed peer. Teach testcontrol to mark a
node as UnsignedPeerAPIOnly (excluding such nodes from traffic-
permitting filter rules, as real control does, so clients don't
discard the packet filter) and make TestFunnel use it so the test
now exercises the same capability checks as production Funnel
traffic.

Updates tailscale/corp#46053
Updates #20739


Change-Id: I3f6b8e2a94d1c07b5a2e9d84f16c30aa79e5d21b

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 08:43:45 -07: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
Brad Fitzpatrick 12e4462ba3 root: make TestLicenseHeaders skip files unknown to git
The license header test walked the whole tree, so scratch files,
worktrees, and other untracked local files made "go test ." fail in a
developer checkout. Ask git for the set of tracked files and only
check those, falling back to checking everything (as before) when the
tree is not a git checkout, such as in a release tarball.

Fixes #20740

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9f3a7c21e5b804d6a2f4c8e19d7b53a6e0c412fd
2026-08-04 10:47:35 +01:00
Brad Fitzpatrick 91c1bbecb1 util/httpm: narrow test cache git dependency
Be cacheable in git worktrees.

Revision to earlier b39ee0445d

Updates tailscale/corp#40359

Change-Id: Ib374d90bf6ee833caabaa3c159ec9f09d991aa41
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-04 10:47:04 +01:00
Brad Fitzpatrick 4c4d1c35f8 ipn/ipnlocal: avoid deadlocks during shutdown
LocalBackend.Shutdown waits for the ACME refresh loop and active SSH
sessions. Both can be blocked acquiring LocalBackend.mu, so waiting while
holding that mutex deadlocks shutdown.

Detach the SSH server under the mutex, then stop both subsystems after
releasing it. Prevent their work from restarting once shutdown begins, and
serialize repeated Shutdown calls with sync.Once.

Add regression tests that verify subsystem shutdown runs without
LocalBackend.mu held.

Updates tailscale/corp#45964

Change-Id: I37ead4f26fbfb5703a83882668d98a8862ba7d67
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-01 13:57:38 -07:00
loowr d645114dd4 ipnauth: set isUnixSock in the ts_omit_unixsocketidentity GetConnIdentity
The ts_omit_unixsocketidentity variant of GetConnIdentity never
performs the *net.UnixConn type assertion, so ConnIdentity.isUnixSock
stays false. ipnserver's Permissions only grants local API access to
unix socket connections, so with this build tag every local API request
is denied read and write access ("status access denied") and the CLI
cannot talk to the daemon at all in --extra-small/--min builds.

Mirror the type assertion from the peercred variant so the omitted
identity build behaves as intended (everyone is an admin when unix
socket identities are compiled out).

Signed-off-by: loowr <loowr@proton.me>
2026-07-31 17:30:11 -07:00
Brad Fitzpatrick 9f7d3f91cd go.mod: bump json-experiment for ExperimentalGlobalSupportFormatTag
We'll need this in corp at least, which means we need this here.

Updates #20220

Change-Id: I5dbf93f2b9ce05658193fc8ba61eb61185637521
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-31 16:38:23 -07:00
Brad Fitzpatrick a5d6c80f86 go.mod: re-bump staticcheck to v0.8.0-rc.1
Commit 33042fb97 (go.mod: bump sigs.k8s.io/controller-runtime to
v0.23.3) was based on a stale tree and accidentally reverted the
staticcheck bump from 7eeb62415 back to v0.7.0. That version's IR
builder panics on the Go 1.27 standard library (unexpected expr:
*ast.KeyValueExpr), breaking staticcheck CI on the Go 1.27 test
branch. Everything else in that commit was intentional k8s ecosystem
upgrades; staticcheck was the only collateral revert.

Updates #20220

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I763a3dcc29e592adcf6979c80d6e720b02c0bb09
2026-07-31 14:42:34 -07:00