Commit Graph
11433 Commits
Author SHA1 Message Date
Brendan Creane e0d95c6d91 ipn/conffile,cmd/tailscale/cli: record the serve front-end protocol in the config file
The services config file stored one protocol per endpoint, taken from
the back-end proxy URL. An HTTPS listener in front of a plain-HTTP
back-end was exported as "http://", and re-applying that file produced a
plain-HTTP listener. The change from TLS to plaintext was silent.

Target now records the front-end (listener) protocol and the back-end
protocol separately. An endpoint is written as the shorthand
"<front>://<host>:<port>" when the back-end is the front-end's default,
and as {"front": ..., "backend": "<proto>://<host>:<port>"} otherwise.
The loader rejects front/back-end pairs the serve apply path cannot
carry.

Every place where a TLS hop could quietly become a plaintext one now
either keeps TLS or reports what it did:

  - get-config exports the listener protocol, so an HTTPS listener
    stays HTTPS on re-apply.
  - Exports are stamped version 0.0.2. In 0.0.1 files the bare
    "https://" shorthand named a TLS back-end. In 0.0.2 it names the
    listener. The loader keeps the TLS back-end for a 0.0.1 file and
    warns that the file should be re-exported. An older client rejects
    a 0.0.2 file instead of misreading it.
  - get-config refuses to export a handler the format cannot represent
    (text, redirect, extra mount points, non-HTTP proxy schemes). With
    --all it skips the service, warns on stderr, and exits nonzero.
  - A Unix socket destination accepts only the front-end's default
    back-end, because tailscaled speaks plain HTTP or TCP to the socket.
    "https+insecure://unix:<path>" is now a parse error.

The exported field conffile.Target.Protocol is renamed to Front and a
Backend field is added. Out-of-tree code that built a Target by field
name needs updating.

Fixes #18381
Updates #19724

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-10-02 15:50:25 +00:00
Alex Chan 9128778b65 types/key: rename NLPublic/Private to TLPublic/Private
The NL prefix is one of the last references to the "Network Lock" name
which we're cleaning up.

Updates tailscale/corp#37904

Change-Id: I751194e0b49447e97a65ed6975e3da680226ce85
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-10-02 16:03:51 +01:00
Tom Proctor 3ce5e20997 go.mod,cmd/cigocacher: migrate to depending on tb (#21627)
Building cigocacher from the corp repo failed because its go.mod was
missing a reference to go-tool-cache. Migrate cigocacher over to tb so
it's not required.

Updates tailscale/corp#49137

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2026-10-02 10:59:48 +01:00
Brad Fitzpatrick 67f8c81e61 logpolicy, net/dnscache: cache log server DNS lookups across dials
logtail's dialer only used dnscache in its fallback path, and built a
new Resolver per dial there, so every new connection to the log server
did a fresh DNS lookup (twice when the first dial failed). On systems
without a caching resolver, that adds up to a lot of queries for
log.tailscale.com.

Share one dnscache.Resolver across all log dialers, and have the
bootstrap path go straight to dnsfallback rather than repeating the
regular lookup. Since the cache TTL is a fixed guess, make
dnscache.Dialer expire a cached entry and look the host up again when
dialing its cached IPs fails, so a moved host isn't stuck on stale IPs.

Updates #15326

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I6a55d72a73ed878faebc15317421a54d9ae293ca
2026-10-01 19:22:39 -07:00
Caleb Crome 12e898010c net/dns: add a local Windows hosts-update control
Allow TS_DEBUG_DISABLE_HOSTS_FILE_UPDATES to opt out of Windows hosts entries locally, using the existing tailscaled-env.txt configuration mechanism. Keep the control-plane node attribute authoritative and preserve upstream behavior when the local option is unset or false.

Cover defaults, local opt-out, nil control knobs, and policy precedence with Windows tests. This change is limited to DNS configuration; no route changes or upstream route-fix cherry-pick are included.

Updates tailscale/tailscale#14327

Signed-off-by: Caleb Crome <caleb.crome@flyzipline.com>
2026-10-01 15:37:30 -07:00
François Marier d33279d752 tstest/natlab/vmtest: wait for tailscaled to break race with tta (#21472)
The agent and tailscaled are started concurrently by gokrazy and
tta can win that race, in which case it answers 502 Bad Gateway
because tailscaled.sock does not exist yet.

This is most likely to be seen on slower machines using TCG.

Updates #cleanup

Change-Id: I4012bd189adfb2565470c814204fcf18545f1017
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-10-01 11:35:16 -07:00
Jonathan Nobels d8c7cafeda feature/exitnodehealth: warn when selected exit nodes are unavailable (#21017)
updates tailscale/corp#33007

When a selected exit node can't carry internet traffic due to a misconfiguration (wrong ID, node
deleted, routes removed, etc), blackhole default routes are installed and all internet traffic is dropped.
That is the correct behavior -- better to drop than leak to the local network -- but it was entirely silent:
the stable-ID lookup in nodeBackend.updateRouteManagerPrefs misses without a log line, and Status
leaves ExitNodeStatus nil. The user sees a healthy Tailscale with no internet. If an admin fat-fingers the
exit node name in an IT policy, for example, it's easy to break every node with zero feedback.

This adds an exit-node-unavailable health warning covering every way the selection can fail to carry
traffic (short of reachability which is a separate concern), reported via exitnodehealth.ArgExitNodeReason.

The warning names the exit node, caching its display name while it is still a peer so the name survives
its departure, and falls back to the stable ID or IP. When the selection is mandated by the ExitNodeID
or ExitNodeIP policy settings, the message tells the user to contact their network administrator instead
of suggesting they pick another exit node.

To test this, set a forced exit node policy with some random ip or node id. The warning has a 5 second
 threshold. It should clear as soon as you change to a proper exit node.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2026-10-01 08:57:20 -04:00
David Bond 689a0c155f cmd/k8s-operator,k8s-operator/reconciler/staticendpoints: support static endpoints for Connector resources (#21172)
Previously ProxyClass spec.staticEndpoints was only implemented for
ProxyGroups and silently ignored when the ProxyClass was referenced by
a Connector. Extract the NodePort Service provisioning, port
allocation, and node ExternalIP discovery logic shared with ProxyGroup
into a new k8s-operator/reconciler/staticendpoints package, and wire
it into the StatefulSet reconciler so that each Connector replica gets
a per-pod NodePort Service, the discovered endpoints are written to
its tailscaled config as static endpoints, and tailscaled listens on
the Services' target port via the PORT env var. Also reconcile
Connectors on Node changes, clean up NodePort Services on scale down,
Connector deletion, and when static endpoints are removed, and
surface the endpoints in the Connector's device status.

Add e2e tests for static endpoints on both Connectors and ProxyGroups.
They verify the per-replica NodePort Services, the PORT env var, the
endpoints reported in status and advertised to control, and the
cleanup on scale down and on removal of the static endpoints
configuration. The tests skip on clusters whose Nodes have no
ExternalIP addresses, such as kind.

Fixes #18819

Change-Id: Ia134916062471e6c0088692834257aafabd0e713

Signed-off-by: David Bond <davidsbond93@gmail.com>
2026-10-01 12:07:16 +01:00
Raphael Fakhri b0e35bf5b5 util/cmpver: don't panic on numbers that overflow a uint64
Compare parsed each numeric field with strconv.ParseUint and panicked
if that failed, so a version with a field of 20 or more digits, such as
a long build stamp, crashed the caller.

Compare digit runs by length after trimming leading zeros, then
lexically. This gives the same results as before for numbers that fit
in a uint64 and works for numbers of any length.

Fixes #21532

Signed-off-by: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com>
2026-09-30 21:41:37 -07:00
Brad Fitzpatrick 5d5607fea2 ipn/ipnlocal: use more buildfeatures consts so the linker drops dead code
Several features with ts_omit build tags were still linked into
minimal builds because LocalBackend called into them unconditionally,
so the linker couldn't prove them unreachable. Guard those call sites
with their buildfeatures constants:

  * app connectors: only subscribe to route update and store events
    when app connectors are included; appc is their only publisher.
    This also drops AdvertiseRoute and UnadvertiseRoute.
  * client update: only subscribe to the tailnet default auto-update
    event when auto-updates are supported, as that's all it affects.
  * syspolicy: only register the policy change watch when system
    policy support is included.
  * advertise routes and exit nodes: only validate AdvertiseRoutes
    when one of the two features is included.
  * netstack: only netstack consults the intercepted TCP ports, so
    don't build the port matching func without it.
  * ssh: don't check SSH prefs, report the "SSH on but unusable"
    health message, or intercept port 22 without SSH support. The
    RunSSH pref is still accepted without effect, as before; port 22
    was previously intercepted by netstack builds without SSH.
  * web client: don't track whether the web client should run when
    it's not included.
  * tailnet lock: peers marked UnsignedPeerAPIOnly are only special
    in that tailnet lock skips their signature checks, and the packet
    filter check guards that exemption. Without tailnet lock, all
    peers from control are trusted anyway, so skip the check.

This reduces the size of a build_dist.sh --min tailscaled binary on
linux/amd64 by 65,536 bytes (0.5%), about 28 KB of it in ipnlocal and
most of the rest in eventbus and slices generic instantiations for the
events no longer subscribed to.

Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8f4d4c0779e84f46f3b076d0fb35514c69a52d8f
2026-09-30 18:34:57 -07:00
Brad Fitzpatrick 654c105f8b tstest/natlab/vmtest, gokrazy, cmd/tailscale/cli: add minimal-build vmtests
Add vmtests that boot pairs of gokrazy VMs whose tailscale and tailscaled
are minimal builds and check that IP traffic flows between them over
WireGuard, so we notice if a minimal build stops doing its one job:
build_dist.sh --extra-small (direct path through NATs), that minus NAT
traversal (DERP across NATs, direct on a shared LAN), and DERP-only.

They found that "tailscale up" built with ts_omit_ipnbus returned before
calling Start or EditPrefs, making it a no-op. Fix that, and have it wait
for the Running state by polling tailscaled's status instead of watching
the IPN bus, so scripts can rely on it as with normal builds.

The extra-small feature list moves to featuretags.ExtraSmall so
build_dist.sh (via a new "featuretags --extra-small") and vmtest share
it. The gokrazy builder gains --output and --go-build-tags flags to build
the variant images, and natlabprep --gokrazy prebuilds them all for CI.

Updates #13038
Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0caa2c7b3adf2399da09b6d07a942f1dc34e8486
2026-09-30 15:53:47 -07:00
Nick O'Neill 3e2dca47c5 VERSION.txt: this is v1.105.0 (#21600)
Signed-off-by: Nick O'Neill <nick@tailscale.com>
v1.105.0-pre
2026-09-30 12:03:36 -07:00
Nick Khyl ab2380a631 net/dns,util/coalescedop: serialize and coalesce DNS flush calls on Windows
Previously, ipconfig /flushdns and ipconfig /registerdns were called
from multiple sites without coordination: the DNS manager's SetDNS
(in a fire-and-forget goroutine), the router's winRouter.Set (synchronously
on every route change), the link change handler, and session unlock.
These could fire concurrently or in rapid succession, spawning multiple
ipconfig processes and putting excessive pressure on the Dnscache service.

In this PR, we introduce a new util/coalescedop.CoalescedOp type that provides
"one in-flight, at most one pending" semantics: Do() is non-blocking,
at most one execution runs at a time, and concurrent/rapid successive
requests coalesce into a single follow-up execution.

We then use CoalescedOp for both ipconfig /flushdns and ipconfig /registerdns,
replacing the fire-and-forget goroutine in windowsManager.SetDNS
and in the session unlock handler in tailscaled.

We also remove the redundant dns.Flush() call from winRouter.Set, since every
meaningful router.Set is followed by dns.Set in Reconfig, which already
flushes.

Updates #21001

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-09-30 10:44:22 -07:00
Noah Ingwers 238d07c5f3 control/controlclient: allow cancellation during map retry backoff (#21133)
Auto.mapRoutine sleeps in BackOff with c.mu held after PollNetMap fails.
Pausing, restarting the map poll, and shutting down need the same mutex
to cancel that wait, so they can be delayed by the retry timer.

Release c.mu after updating inMapPoll and snapshotting paused. PollNetMap
waits for its map-session callbacks before returning, so accessing the
backoff here cannot race with UpdateFullNetmap resetting it.

Add a synctest regression that checks mutex availability during backoff
and verifies that pausing interrupts the wait without advancing time.

Fixes #21132

Change-Id: I2d5b5a71c5857be60507d4d6ec9e2e0fbd8cf40a
RELNOTE: Reduce pause and shutdown delays during control connection retries.

Signed-off-by: Noah Ingwers <98993329+noah-ing@users.noreply.github.com>
2026-09-30 12:15:02 -04:00
chaosinthecrd 57b3d3591d cmd/k8s-operator: base egress Pod readiness on EndpointSlice membership
The egress Pods readiness reconciler decided whether a Pod could route
traffic by calling each egress Service's health check through its
ClusterIP up to replicas*3 times and looking for a response from that
Pod. That is O(replicas) calls per Service per reconcile, and a Pod
joining a large ProxyGroup is only sampled 3 times on average, so late
Pods often miss and wait for another 5s requeue. The call only goes
through kube-proxy on the operator's own node, so it does not show that
other nodes route to the Pod either.

egress-eps-reconciler already adds a Pod to an egress Service's
EndpointSlice only once the Pod's state Secret shows the proxy has set up
routing for that Service. This sets the readiness condition once the Pod
is an endpoint in an EndpointSlice of every egress Service for the
ProxyGroup, and removes the health check calls.

Updates tailscale/corp#39464

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-09-30 16:28:00 +01:00
Brad Fitzpatrick e987d72be2 feature/featuretags: add MinTags, use it in three places
cmd/featuretags --min, cmd/tsconnect/wasmbuild, and a cmd/tailscaled
test each computed "omit every feature except these" on their own.
Move that into featuretags.MinTags so other Go code (such as upcoming
natlab vmtests of minimal builds) can compute the tags without running
cmd/featuretags. No behavior change.

Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ie0a5faf1f1d3e4e035326572b16f20180671d1c4
2026-09-30 07:42:13 -07:00
James Tucker f5f326030b net/dns: preserve full OS resolver detail in Apple primary mode
When quad-100 must be installed as the OS primary resolver (Apple Mode
B), its catch-all forwarders were blended from base-config nameserver
IPs alone: non-standard ports, search domains, and DoH/DoT endpoints
were lost, and a base read that returned no resolvers silently
installed a catch-all with no forwarders at all, breaking all public
DNS while reporting a healthy DNS configuration.

Introduce OSConfig.Resolvers, which OSConfigurators able to recover the
underlying resolver configuration at full fidelity populate for the DNS
manager only: it never reaches the OS, and Equal ignores it so
configuration application is unaffected. Blend it into the catch-all
route verbatim, falling back to plain IP:53 resolvers derived from
Nameservers.

Teach the forwarder to dial arbitrary https:// resolvers instead of
only well-known public providers: at their BootstrapResolution
addresses when present, or at the URL's own host when that is an IP
literal, so enterprise DoH endpoints recovered from the OS can be
forwarded to over DoH rather than plaintext DNS.

Reject an empty base config on Apple Mode B: keep the previous
configuration, use the upstream empty-base health warning and medium
severity, and let an extension-triggered recompile retry instead of
installing a catch-all that cannot forward. Expose the same missing-
resolver error to OS configurators so bridge-reported absence uses this
warning too, while genuine configuration-read failures remain distinct.

Extend the Apple mode tests with empty-base and full-fidelity blend
coverage, and update the iOS primary-mode cases to model the real
NetworkExtension base read (LAN resolvers and search domains) rather
than the silently empty catch-all.

RELNOTE: Fix silent public DNS breakage when Tailscale must be the system DNS resolver on Apple clients.

Updates #20341
Updates tailscale/corp#45534
Updates tailscale/corp#48693

Change-Id: Ie7e277b0fd39d2b91b1e770f8f01d688e02b4d49
Signed-off-by: James Tucker <james@tailscale.com>
2026-09-29 16:11:57 -07:00
James Tucker 0fe0fe4524 net/dns: align Apple resolver scoping with search-domain behavior
Apple tunnels derive global search domains from match domains. Scope
simple iOS and sandboxed macOS configurations to the full MagicDNS route
set; keep custom split resolvers and uncovered forward/PTR records behind
a primary resolver. Respect scoping controls and reject unsafe fallback
when base DNS cannot be read.

Add mode-selection and reverse-coverage tests for both Apple platforms.

RELNOTE: Fix MagicDNS routing and split-DNS search scoping on Apple clients.

Updates tailscale/corp#48693

Change-Id: I50b5641db466f77c19f911255748e12a10d0a5fe
Signed-off-by: James Tucker <james@tailscale.com>
2026-09-29 16:11:57 -07:00
Brad Fitzpatrick 70422c917a version/mkversion: allow supplying the complete version via env vars
mkversion derives the version from git history, which a shallow clone
lacks (the change count silently comes out as 0) and a tree without
.git can't provide at all. If TS_VERSION_LONG is set, InfoFrom now
takes the version from it plus TS_VERSION_GIT_HASH (and the EXTRA/DATE
variants), runs no git, and derives everything else through the same
mkOutput as the git path. The result's Long must reproduce the input
exactly or InfoFrom errors.

Updates tailscale/corp#44945

Change-Id: I9c1247ea5d1c6c52c823c2205447b5c7ee8e778c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 15:42:05 -07:00
Jordan Whited 3fb612f598 go.mod,wgengine/wgcfg: export wireguard-go replay drops as clientmetric
tailscale/wireguard-go@417aef3 is the only wireguard-go commit
introduced by this dep update.

Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-29 15:30:53 -07:00
Becky Pauley a00fd3273b cmd/k8s-operator: suppress egress-pod-readiness backoff logs
The egress Pod readiness reconciler polls each Service's health check
endpoint up to replicas*3 times per Pod. A freshly created Pod is initially
unreachable, so these backoff log lines carry no value and generate
significant noise.

Discard these logs so the per-poll message is no longer logged (the more
useful message "Pod is not yet added as an endpoint for all egress targets,
waiting..." is still logged once per reconcile).

Updates tailscale/tailscale#21079

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-09-29 21:31:54 +01:00
Andrea GottardoandBrad Fitzpatrick 5e21c7cc10 logtail: remove LowMemory mode
LowMemory shrank the pending ring buffer, upload batch size, and text
truncation limit for iOS, back when network extensions were limited to
15 MB. All supported iOS versions now allow 50 MB, so the savings are
negligible and the extra code paths are not worth maintaining.

Fixes #13685

Change-Id: Ic0c26f899ce00f105399c7b1e687e2b92194687e
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2026-09-29 13:16:16 -07:00
Brad Fitzpatrick 7649cf9501 version/mkversion: fix commit date lookup in the tailscale.com checkout path
infoFromDir passed --format=%%ct to git log, which git renders as the
literal string "%ct", so VersionInfo.GitDate was never a timestamp for
builds made directly from a tailscale.com checkout. The corp path in
infoFromCache already used the correct %ct.

Updates tailscale/corp#44945

Change-Id: I28d193019a8dd3d2bdd165da5e4584df1964afd6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 12:07:08 -07:00
Claus Lensbøl b31be2cd1e tstest/natlab/{vnet,vmtest}: clean up natlab inconsitencies (#21483)
There were a handful of inconsitencies in natlab with the biggest being.
 - Pair and grid tests living under tstest/integration and using the old
   test style.
 - Inconsistent use of e vs env.
 - Methods for actions on node living on the vnet.Node rather than being
   a method on vmtest.Env

Clean up these inconsitencies.

Updates #cleanup

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-29 14:57:10 -04:00
Francois Marier eb79f3e33e tstest/natlab/vmtest: fix test flake
It's possible for multiple direct connections to be counted and
these tests are successful as long as it's more than one.

Fixes #21578

Change-Id: I979efc92a43f78d0e51d602d96e61828e6fabdff
Signed-off-by: Francois Marier <francois@tailscale.com>
2026-09-29 10:56:17 -07:00
Brad Fitzpatrick bcc7e46f42 tstest/integration/testcontrol: add experiment.reco implementation
Select the published reco-backed test server with a build tag while
preserving the default implementation and existing caller API. Pin the
public module with its proxy-verified checksums; no workspace is needed.

Accept incremental peer messages in the TSP test. Exercise legacy map
semantics with the default server and version-floor rejection with reco,
including current-version streaming, without manual test skips.

Document GOFLAGS usage so child Go builds inherit the experiment tag.

Updates tailscale/corp#49097

Change-Id: I9b3e086c4c6a9a57a076e76b883422875c843f20
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 10:25:02 -07:00
Kristoffer Dalby cf7d4c6015 ipn/ipnlocal: report peers dropped by a full netmap as PeersRemoved
Peer-change watchers upsert PeersChanged, so peers missing from a full
netmap were never removed from their view.

Updates #15660

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-09-29 19:23:51 +02:00
Kristoffer Dalby c4a66d1ce1 ipn/ipnlocal, tstest/largetailnet: test removals bundled with full netmaps
A peer removal sent with any MapResponse field that forces a full netmap
rebuild never reaches IPN bus watchers; these tests fail.

Updates #15660

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2026-09-29 19:23:51 +02:00
Brad Fitzpatrick 5758b2aa57 derp/derpserver: add app filter, connected-time sort, and JSON output to /debug/clients/
Add three things to the connected clients debug page:

app=NAME narrows any of the existing filters to connections that
advertised that app name, and may be repeated to match any of several
(app=tailcat-server&app=tailcat-client). On its own it applies to all
connections. An empty app= matches connections that sent no app name.
App names in the table link to their filter, and the next-page and
sort links carry the app filter along.

sort=connected walks by connection time, ascending being longest
connected first, with -connected for newest first. The next-page
links use the connection time in Unix nanoseconds as the cursor; by
hand, after= also accepts a duration such as 30m, meaning connections
that have been up that long, which is the natural way to ask for
"everything older than half an hour".

format=json returns the page as a JSON object with the filter
description, the matching connection and key counts, how many
connections remain after the page, the next page's relative URL, and
the client rows, so the page can be walked from curl or a script the
same way a browser follows the next links. Rows gain a connectedAt
timestamp alongside the rounded connected duration.

Updates tailscale/corp#48933

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ib5d2e7c40a9f13e8a6d7c2b5f9e0a4d3c8b17e62
2026-09-29 09:18:15 -07:00
Brad Fitzpatrick b8dc4061b0 net/socks5: don't half-close the read side after EOF, tear down on error
Shutting down a socket's read side after its peer's FIN has arrived does
nothing on the wire and fails with ENOTCONN on macOS (and on Linux once
the socket reaches TIME_WAIT), so every cleanly half-closed connection
was logged as a failure and TestTCPHalfClose failed on macOS. Only
half-close the destination after EOF.

When one direction fails, one of the two sockets is dead. If the failure
was writing to it, the other direction is reading from that dead socket
and finishes on its own once it has drained what arrived before the
failure, so leave it alone rather than truncate the data. If the failure
was reading from it, the other direction may be blocked reading the live
peer, so close both connections to unblock it; nothing deliverable is
lost in that case.

Telling the two apart means wrapping the destination, which disables the
kernel splice fast path when both ends are bare TCP connections (about a
third of relay throughput on Linux loopback). That is accepted here: the
common userspace-networking and proxymux configurations wrap the
connections anyway, so they never had the fast path.

The regression was introduced by 027e249fcf (#21359), merged 2026-09-17.

Tested on Linux and on macOS 27 (arm64): the old test failed 49 of 50
runs on macOS; the new tests pass 200 iterations there and pass under
the race detector on Linux.

Fixes #21522
Updates #20883

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7c2e4a9d1b8f5e3c6a0d2f4b8e1c9a7d3f5b6e2c
2026-09-29 08:50:22 -07:00
Michael Ben-Ami (on Claude) a0e471a35b feature/conn25: split client and connector code into their own files
conn25.go had grown to hold the extension, the Conn25 type, the config,
the DNS rewriting, and both the client and connector implementations.
Move the client struct and its methods to client.go, and the connector
struct and its methods to connector.go.

appAddr, transitIPExpiryEntry, and connectorTransitIPExpiry are used
only by the connector, so they move to connector.go. addrs moves to
client.go because it is a client-side concept, even though the
extension's send path in conn25.go and addrAssignments.go also refer
to it. Everything else stays in conn25.go.

Test functions covering client and connector behavior move to
client_test.go and connector_test.go correspondingly.

This is a pure code move: no declaration is added, removed, or edited,
and the relative order of everything is unchanged.

Updates #cleanup

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-09-29 10:21:45 -04:00
chaosinthecrd 06e3480a85 cmd/k8s-operator: clean up HA Service when proxy-group annotation removed
When the tailscale.com/proxy-group annotation is removed from a Service
that was exposed on a ProxyGroup, the HA Service reconciler returned early
on the empty annotation before reaching its cleanup path. The Tailscale
Service was left advertised and the operator's finalizer was never removed,
so the Tailscale Service leaked and the Kubernetes Service wedged forever in
Terminating once deleted.

The annotation is the only place the ProxyGroup name was recorded, and it is
gone by the time cleanup needs it. This commit encodes the ProxyGroup name in
the finalizer, which survives any modification of the resources annotations.
On reconcile, an empty annotation with our finalizer present now recovers the
ProxyGroup name from the finalizer and runs cleanup. Services carrying the bare
legacy finalizer are migrated to the encoded form while the annotation is still
present.

Updates #19922

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2026-09-29 14:55:00 +01:00
Brad Fitzpatrick fe91043315 wgengine/magicsock, net/netcheck, feature/featuretags: add udptransport and nattraversal features
Contining the trend of making everything be optional that could
possibly be optional, this makes the magicsock UDP underlay transport
be optional. (mostly replacing a bunch of GOOS != "js" checks in the process)

Omitting udptransport (ts_omit_udptransport) makes magicsock DERP-only: no
UDP sockets, no advertised endpoints, and netcheck measures DERP latency
over HTTPS. Omitting only nattraversal keeps disco ping/pong to peers'
advertised endpoints but drops STUN, call-me-maybe, the peer relay client,
the endpoint tracker, and UDP lifetime probing.

Both are plain linker dead-code elimination via buildfeatures
constants. (as oppposed to moving the code all over into feature
packages and indirecting through hooks) The minimal linux/amd64
tailscaled shrinks by 258 KB without nattraversal and 561 KB without
both.

Updates #12614

Change-Id: Ife10804b48a168d9481e4321c18ed8fc85fa71f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-29 06:09:17 -07:00
Brad Fitzpatrick 5d35baeb87 wgengine/netstack: don't half-close the backend's read side after EOF
After copying from the backend to the client until EOF, forwardTCP shut
down the backend socket's read side. That does nothing on the wire, and
once the backend's FIN has arrived, macOS rejects shutdown(SHUT_RD) with
ENOTCONN (Linux does too once the socket reaches TIME_WAIT), so every
forwarded connection logged "backend -> client close connection: ...
socket is not connected". Keep only the CloseWrite calls, which are what
propagate the half-close.

The regression was introduced by 04d24cdbd4 (#16462), merged 2025-07-07.

Tested on Linux and on macOS 27 (arm64): the wgengine/netstack tests
pass on both. The same ENOTCONN failure was reproduced on macOS via the
identical pattern in net/socks5 (#21522).

Updates #21522
Updates #16462

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3e9b1d7f4a2c8e6b0f5d9a3c7e1b4f8d2a6c0e5b
2026-09-29 05:29:48 -07:00
chleeandJordan Whited dca60dd77d net/batching: preserve IPv6 zone in WriteBatchTo
WriteBatchTo copied the destination IP and port into the pooled
net.UDPAddr handed to sendmmsg(2), but not the zone. For an IPv6
link-local destination the sockaddr then had no scope ID, and Linux
failed the send with "cannot assign requested address". Disco goes
through WriteToUDPAddrPort, which keeps the zone, so magicsock could
pick a link-local path whose WireGuard packets, sent via WriteBatchTo,
never left the host.

Set the zone on every write, so that a destination without one also
clears the zone left over from an earlier write that reused the
pooled address.

Fixes #21411

Co-Authored-By: Jordan Whited <jordan@tailscale.com>
Signed-off-by: chlee <sourcehatchery@gmail.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-28 20:48:51 -07:00
Brad Fitzpatrick b4e3a5299c cmd/tailscaled, safesocket: add --windows-mode=dev
This adds a tailscaled string flag "--windows-mode" which accepts two
possible values: the empty string (default) to get the normal behavior
(tailscaled running as an admin, usually as a service), and "dev", to
make the safesocket named pipe path be at a location that regular
users (non-admins) can create.

It then modifies the safesocket client side (as used by the CLI) to
try the dev mode path too on failure.

This lets people work on tailscaled.exe+tailscale.exe in a terminal
easily during development, as either an admin or non-admin. (This
used to work prior to the move away from TCP localhost to named pipes
for safesocket on Windows)

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9c1e3a5b7d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c
2026-09-28 13:26:52 -07:00
Brad Fitzpatrick 48ec6284d3 safesocket: let a non-administrator listen on the Windows named pipe
The pipe's security descriptor named the Administrators group as the
pipe's owner (O:BA) and primary group (G:BA). Only administrators may
assign that SID as an owner, so a tailscaled run by a regular user (in
userspace-networking mode with its own --socket) died at startup with
"This security ID may not be assigned as the owner of this object". The
tests already had to swap in an empty descriptor to run unelevated.

Neither part did anything for us. The primary group is never consulted by
Windows access checks; it exists only for POSIX and NFS compatibility. The
owner grants exactly one thing beyond the DACL: the right to read and
rewrite the pipe's DACL. Whoever creates the pipe already has that in
practice. For the default pipe name under ProtectedPrefix\Administrators,
the creator must be an administrator (only administrators may create pipes
with that prefix), so naming Administrators as owner gave them nothing
new. A regular user creating a pipe of some other name runs the process
that owns it and controls its DACL anyway. Who may connect is decided only
by the DACL, which is unchanged: read and write for Users and LocalSystem.

So drop the owner and group and let the creator own the pipe. While here,
drop the AI, OI, and CI flags too: they concern inheritance to child
objects, which a named pipe doesn't have, and were copied from a file
system descriptor. The tests now use the real descriptor. Also give a
pointer when listening on the default ProtectedPrefix\Administrators pipe
fails with access denied.

Updates #2791

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8a1c3e5f7b9d2e4a6c8b0d2f4e6a8c0b2d4f6a8c
2026-09-28 13:26:52 -07:00
Basavaraj S mandMike Jensen a40d145b21 derp/derphttp: reject invalid DERP node hostname before proxy CONNECT (#21038)
* derp/derphttp: reject invalid DERP node hostname before proxy CONNECT

When a DERP client reaches a node through an HTTP(S) proxy,
dialNodeUsingProxy writes the CONNECT request by hand and puts
net.JoinHostPort(n.HostName, port) into both the request line and the
Host header. n.HostName comes from the control-supplied DERP map and
net.JoinHostPort does no sanitizing, so a hostname carrying CR/LF was
written verbatim into the plaintext request sent to the proxy. That let
whoever populated the DERP map inject extra headers, or a second
pipelined request, into the connection to the operator's proxy.

Validate n.HostName with httpguts.ValidHostHeader at the top of
dialNodeUsingProxy, before the proxy is dialed, and also reject the
empty hostname, which ValidHostHeader accepts. DNS names and IP
literals continue to work.

Add a table-driven test that runs accepted and rejected hostnames
against a fake proxy and checks the CONNECT target that goes out.

Fixes tailscale/corp#48122

Signed-off-by: basavaraj-sm05 <basavaraj@digiscrypt.com>
Co-authored-by: Mike Jensen <mikej@tailscale.com>
Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-28 12:56:36 -06:00
Mazdak Nasab ecedffff5b feature/conn25: unmap addresses in TransitIPRequest
Unmap both transit and destination IPs in
TransitIPRequest to prevent unpredictable mappings.

Fixes https://github.com/tailscale/corp/issues/48781

Change-Id: I5aea2bf4e5ff5240c66f20c71cfa1ceba1722622
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-28 11:11:52 -07:00
Mazdak Nasab 8a119e1ec0 feature/conn25: skip AAAA records with IPv4-in-IPv6 addresses
Skip destination IPs from AAAA records that are
in ipv4-in-ipv6 format.

Fixes https://github.com/tailscale/corp/issues/48781

Change-Id: I5eeaa9c998c79250d2e6a943fa9f26ed7feb5ec6
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-28 11:11:52 -07:00
Brad Fitzpatrick 7bb87d3614 derp/derpserver, cmd/derper: add /debug/clients/ page listing connected clients
The derper debug pages had no way to see which clients were connected.
The expvar gauges only give counts, /debug/check only says whether the
counts agree, and /debug/traffic only reports connections that moved
bytes since its last tick, and only if ss is installed.

Add /debug/clients/, which by default serves an index page with a form
to pick one of four filters: ?all lists every connection, ?ip=1.2.3.4
and ?cidr=1.2.0.0/16 list connections from an address or prefix, and
?key=nodekey:... lists the connection(s) for one node key. Each row
shows the connection number, key, remote address, connection age,
flags (home, mesh, prober, notideal, dup/active/disabled), protocol
version, app name, per-connection rx/tx packet and byte counts, and
the estimated unique sender count.

Big derpers have far too many connections for one page, so results
are paginated with keyset cursors rather than page numbers: sort=key,
ip, conn, rx, tx, rxpkts, or txpkts (with a leading - for descending)
picks the walk order, limit=N the page size, and after=X resumes after
that value of the sort field. The next-page links add afterconn=N so a
page boundary that falls among connections sharing a value (duplicate
keys, one IP with many ports, equal counters) resumes exactly. Column
headers link to the other sort orders.

The walk under Server.mu does only a filter match, a cursor comparison,
and at most a bounded-heap operation per connection, so connections
before the cursor are discarded without being copied and at most limit
entries are ever kept. Only the summary counts (matching connections
and keys) look at every connection. Snapshots are taken and the page
rendered after the lock is released, so a slow debug client can't
stall the server. A benchmark with 100k connections takes about 10ms
per page.

There were no per-connection traffic counters before, only the
server-wide ones, so sclient gains four atomic.Uint64 counters (rx/tx
packets and bytes, counting data packets like the server-wide ones)
bumped alongside them. That's 32 bytes per connection. For the counter
sorts, the value is loaded once per connection during the walk and
used for both the cursor test and the heap order, so the order stays
consistent while the counters keep changing.

The sclient preferred field becomes an atomic.Bool so the page can
report which connections are the client's home DERP; it was previously
only touched by the run goroutine.

Updates tailscale/corp#48933

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I4e9b7c2d5a83f61b0e7d2c94a5f8b3e16d7c0a29
2026-09-28 10:14:36 -07:00
Dep Updater 48c7fbcc56 go.mod: bump 4 modules
* github.com/gokrazy/firmware: https://github.com/gokrazy/firmware/compare/527ce0ed43cf...f966a2e0e4b0
* github.com/gokrazy/kernel.amd64: https://github.com/gokrazy/kernel.amd64/compare/21771250d660...5a347b66f180
* github.com/gokrazy/kernel.arm64: https://github.com/gokrazy/kernel.arm64/compare/ac43a676b0b4...3998e71b7b1d
* github.com/gokrazy/rpi-eeprom: https://github.com/gokrazy/rpi-eeprom/compare/b80c62cf428d...6a92fdda349c
* github.com/gokrazy/breakglass: already current
* github.com/gokrazy/gokrazy: already current
* github.com/gokrazy/kernel.rpi: already current
* github.com/gokrazy/serial-busybox: already current

Triggered by the gokrazy-bump workflow's schedule.

Updates #1866

Signed-off-by: Dep Updater <noreply+dep-updater@tailscale.com>
2026-09-28 07:07:05 -07:00
Brendan Creane 2d4379386a net/dns: don't take over DNS when the OS has no upstream resolvers (#20794)
On backends that can't do OS-level split DNS, Tailscale forwards the default
route to the system's own resolvers, which it reads out of the OS config. At
boot that config may not be populated yet, because NetworkManager or
systemd-resolved haven't run, and Tailscale took over regardless: it pointed
the OS at 100.100.100.100 but compiled an empty "." route, so every
non-Tailscale name failed to resolve for the rest of the process's life.

Fail compileConfig instead, which leaves the OS resolvers in place, and
retry the last config with a bounded backoff until resolvers appear. A
health warning explains why MagicDNS is inactive in the meantime.

Sandboxed macOS and iOS are exempt. There the network extension reapplies
the config itself when the OS nameservers change, and quad-100 as the
primary resolver is what keeps tailnet names resolving while the base config
is still empty. Dropping the exemption is tracked in tailscale/corp#48962.

This also changes the outcome on an openresolv host with no snippets
registered. Since the fix for #20825, openresolv reports an empty base config
there and tailscaled took over with no upstream, so every public name got
SERVFAIL. Such a host now keeps its resolv.conf and shows the health warning
instead. TestOpenresolvDNS checks that outcome.

TestSplitDNSEmptyBaseConfig covers the boot race end to end in natlab: it
empties resolv.conf on a guest using the "direct" backend, checks that
tailscaled leaves it alone, then adds a resolver and checks that both tailnet
and public names resolve.

Fixes #20341

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-09-27 10:20:42 -07:00
Murad Rabadanov fe0d7d1dc5 tstun: skip netstack checksum handling for raw injections
Updates #18013

Signed-off-by: Murad Rabadanov <the21.21@mail.ru>
2026-09-26 14:58:38 -07:00
Dep Updater 6b3a45f14e go.mod: bump github.com/go4org/hashtriemap
* github.com/go4org/hashtriemap: https://github.com/go4org/hashtriemap/compare/45fcf11fca0e...44e5305f85d9

Triggered by @bradfitz via the bumpdep workflow.

Updates #21485

Signed-off-by: Dep Updater <noreply+dep-updater@tailscale.com>
2026-09-25 16:03:48 -07:00
Martin Minkus 27b2a7d1a1 wgengine/router/osrouter: leave pf and forwarding alone in FreeBSD netstack mode
FreeBSD handles subnet routes in netstack by default, but the router
still installed its pf NAT rules whenever SNATSubnetRoutes was set,
which is the default. Every FreeBSD node, subnet router or not, loaded
and enabled pf, inserted anchor references into the host's main
ruleset and loaded NAT rules that netstack never needed, since
netstack dials subnet destinations from the host's own addresses.
It also flipped the forwarding sysctls for any advertised route.

Only do either when the kernel path is opted into with
TS_DEBUG_NETSTACK_SUBNETS=false and routes are advertised.

TestSubnetRouterFreeBSDManyFlows ran in netstack mode since the default
changed, so it no longer exercised pf at all. Opt it into the kernel
path and assert the anchor holds NAT rules, and have
TestSubnetRouterFreeBSD assert the default mode leaves pf untouched.

Also drop the stale claim in handleSubnetsInNetstack that the pf NAT
rule never matches; that was the (self) pool bug, since fixed.

Updates #21450

Change-Id: Ibf91a676a026cebb29f64e39a64fe8e75488360b
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-09-25 12:10:53 -07:00
Brad Fitzpatrick d4f2bb4cff ipn/ipnlocal: skip the kernel peerapi listener on FreeBSD when netstack is present
FreeBSD is a weak-host stack like Linux, so a LAN-adjacent machine can
send a SYN for a node's Tailscale IP to the node's NIC and get the
kernel's SYN-ACK from the peerapi port, confirming which tailnet
identity that MAC address belongs to. Linux closes this with
SO_BINDTODEVICE and macOS/iOS with IP_BOUND_IF, but FreeBSD has no
per-socket interface bind (only IP_RECVIF and SO_SETFIB).

Instead, treat FreeBSD like Android: when netstack is present, don't
create a kernel-level peerapi listener at all and use the fake listener,
since netstack already intercepts peerapi connections from peers in
userspace and the kernel socket only ever served the local host. When
netstack is compiled out (ts_omit_netstack), the kernel listener is the
only way to serve peers, so keep it and log that it must be restricted
with pf.

Along the way, tailscaled's own pf source NAT rule on FreeBSD turned out
to rewrite the source of such a SYN-ACK to the LAN IP and a random port,
so a plain connect() from the LAN never completes. The SYN-ACK still
leaks, though, so the natlab test now watches for SYN-ACKs on the
attacker's NIC with tcpdump instead of checking for a completed
handshake, and gains a FreeBSD variant. With the fix disabled, that
variant fails on the leaked SYN-ACK from the peerapi port.

Fixes #21419
Updates tailscale/corp#48248

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0d2de4ebc9bae28450d500bd7b6ddbfa70f9052f
2026-09-25 12:10:40 -07:00
Mike Jensen 94ea82ab55 ipn/localapi: restrict debug-log access to PermitWrite (#21447)
The debug-log endpoint was gated on PermitRead, which any local user connecting to the world-writable tailscaled socket passes. Although forged log entries are always possible, we should limit the ability for local users to influence that flow with respects to a given node.

This change requires PermitWrite for debug-log, matching the trust level of every other mutating debug endpoint. Also add a buildfeatures.HasDebug guard to debug-dial-types for parity with serveDebug.

Fixes tailscale/corp#48143

Change-Id: I0c0044b6b44fe7cbfb6734ac18bca3dc36eaffbf

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-25 12:29:56 -06:00
Jordan Whited 7f51756f2e go.mod,wgengine/wgcfg: add wireguard cookie reply metric
This also pulls in tailscale/wireguard-go@cbffc0a.

Updates tailscale/corp#48747

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-25 11:10:40 -07:00
Brad Fitzpatrick ec5d51098a derp/derpserver: bound dup client sendHistory growth
When two connections share a node key, they form a dup client set and
noteClientActivity records each sending connection in the set's
sendHistory. It appended on every frame whenever the sender was not the
immediately previous one, and nothing trimmed the slice while both
connections stayed alive. Two connections taking turns sending therefore
grew sendHistory by one *sclient per frame without bound.

Under the default lastWriterIsActive policy nothing ever stops that
growth, so a malicious client (which controls its own node key) or a
buggy one that keeps two connections alive and both sending could leak
server memory, roughly 8 bytes per frame, for the life of the
connection pair.

Record the sender by moving it to the end of sendHistory and dropping any
earlier occurrence, so each connection appears at most once and the slice
stays bounded by the number of connections in the set. This preserves the
existing behavior: the fighting check under disableFighters still runs
before the move and still disables everyone on the first repeat, and
removeClient still promotes the previous speaker from the slice tail.

Fixes tailscale/corp#48884

Change-Id: I06198178e6ab0d7e2f04c1dc4c09eafcb16ace46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-25 09:51:11 -07:00