Commit Graph
1461 Commits
Author SHA1 Message Date
Brendan Creane 1e69418c29 net/dns: fix openresolv DNS config when no other snippets exist (#20996)
openresolv exits 2 when no config snippets are registered; treating that
as a failure aborted the entire DNS reconfiguration. Filtering out our own
snippet could also leave "resolvconf -l" with no arguments, which lists
every snippet including ours and would make quad-100 its own upstream.

Fixes #20825

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-08-27 14:42:26 -07:00
James Tucker a51787201b net/tshttpproxy: respect Windows WPAD registry settings
If web proxy auto discovery is disabled system wide, net/tshttpproxy now respects this configuration and avoids connection to the proxy system service.

Updates #16813
Updates tailscale/corp#29168
Fixes tailscale/corp#38489

Signed-off-by: James Tucker <james@tailscale.com>
2026-08-27 14:25:46 -07:00
Claus Lensbøl fb27d87e02 net/tstun: do not filter TSMPDiscoAdvert inbound (#21009)
The messages needs to be received so that a node not participating in
caching can talk to a node that is participating in caching but is
unable to share its key over control.

Fixes #21008

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-08-27 14:48:00 -04:00
Simon Law 0e84b4a3a0 tailcfg: replace int with DERPRegionID for additional type safety (#20646)
Historically, when DERP regions were switched away from strings to
numeric identifiers in PR #14641, tailcfg.Node.HomeDERP was declared
as an int instead of its own type.

This PR declares a new tailcfg.DERPRegionID type, represented by an
int64, and converts the following fields to use this type:

- netcheck.Report.PreferredDERP
- netcheck.Report.RegionLatency
- netcheck.Report.RegionV4Latency
- netcheck.Report.RegionV6Latency
- tailcfg.DERPHomeParams.RegionScore
- tailcfg.DERPMap.Regions
- tailcfg.DERPNode.RegionID
- tailcfg.DERPRegion.RegionID
- tailcfg.NetInfo.PreferredDERP
- tailcfg.Node.HomeDERP
- tailcfg.PeerChange.DERPRegion
- tailcfg.PingResponse.DERPRegionID

Note that the original field was an int, while the new field is backed
by an int64. This change makes DERPRegionID the same size on both
32-bit and 64-bit architectures.

Fixes: #20165

Change-Id: Ic6f795a6d791dd16f756f246d5a02085443e212f

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-08-19 16:25:50 -07:00
Amal Bansode 0f02428a20 net/traffic: port some rendezvous hasher testcases (#20899)
Port some tiny testcases that apply to our corp impl of the rdv hasher
to OSS as well. This is in preparation for cutting over to the OSS
impl exclusively.

Updates tailscale/corp#46471

Signed-off-by: Amal Bansode <amal@tailscale.com>
2026-08-18 09:56:45 -07: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
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
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
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
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
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
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
Jordan Whited 014d5bd9e4 net/packet,wgengine: update TSMPDiscoKeyAdvertisement docs
Updates #20081
Updates #20494

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-07-31 09:53:25 -07: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
Brad Fitzpatrick 0bbe6394de net/dns/resolver: bound negative caching with SOA records, lower positive TTL
The MagicDNS resolver answered NXDOMAIN and no-data queries with no
SOA record in the authority section. RFC 2308 says such responses
should not be cached, but some resolvers cache them anyway on their
own schedule: macOS's mDNSResponder seems to cache them for a really
long time, so a name queried shortly before a node was renamed to it
didn't start resolving until something flushed the cache, such as
toggling Tailscale off and on.

Attach the zone's SOA record to negative responses for domains we're
authoritative for, advertising a 10 second negative-caching TTL. The
SOA serial is the response time in unix seconds; nothing consumes it
(no secondaries, no zone transfers), but it's at least monotonic.

Also lower the TTL of positive answers from 600 seconds to 5. The
source of truth is local and in-memory, so re-queries are nearly
free, while anything cached downstream delays clients noticing node
renames for the full TTL.

Updates tailscale/corp#45631

Change-Id: Ifb73874c8724f7d95ad7950dcb481f009acab9a4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-28 11:52:50 -07:00
Rayan Salhabandcyphercodes 5927c1864f net/netmon: report disappeared interface IPs (#20621)
InterfaceIPDisappeared is documented as true when an address existed in the
old network state and is absent from the new network state. The
non-nil-state predicate was reversed, so it reported addresses that
appeared instead.

Fix the predicate and add table coverage for disappeared, appeared,
unchanged, and unknown-state cases.

Fixes #20614

Change-Id: Ieb7a8c5e13d23e868e3ffe0b99e20fb14ba2245e

Signed-off-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-07-28 12:57:27 -04:00
Brad Fitzpatrick b3c259bd5c net/netutil: add test coverage for ipForwardingEnabledLinux per-interface reads
The existing test only exercised the not-found-interface path. Now that
ipForwardingEnabledLinux opens its sysctl key with os.OpenInRoot
(840c6e3d3, #20572), also verify that the global keys and the
per-interface keys for every interface actually present on the machine
can be read without error, for both IPv4 and IPv6.

Updates #20572

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ie204a163ab9f8670abedd79a4ac81e400f71aab7
2026-07-22 13:33:20 -07:00
basavaraj-sm05 840c6e3d3d net/netutil: confine ipForwardingEnabledLinux read with os.OpenInRoot
Signed-off-by: basavaraj-sm05 <basavaraj@digiscrypt.com>
2026-07-22 12:36:27 -07:00
Fran Bull 8df4816be4 net/packet: generate ICMP destination unreachable packets
Updates tailscale/corp#34257
Signed-off-by: Fran Bull <fran@tailscale.com>
2026-07-20 07:33:00 -07:00
Brad Fitzpatrick f65372c9ba net/bakedroots: add LetsEncrypt Generation Y roots (YE, YR)
LetsEncrypt announced its new "Generation Y" root hierarchy on
2025-11-24 and switched its default ACME profile to issue from the new
roots in May 2026. Our baked-in fallback root store only contained the
Generation X roots (ISRG Root X1 and X2), so chains terminating at the
new ISRG Root YE (ECDSA P-384) or ISRG Root YR (RSA 4096) roots failed
to verify when the system roots were also missing them.

Add both new self-signed roots, fetched from
https://letsencrypt.org/certificates/ (valid 2025-09-03 to 2045-09-02).

Also add a live network test, run by default in CI only (or with
--run-live-lets-encrypt-test), that verifies the baked-in roots alone
are sufficient to validate LetsEncrypt's per-root test endpoints for
X1, X2, YE, and YR.

Fixes #20527

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ic69076d8ae5aae08db167095745e92c53357afe3
2026-07-20 07:23:35 -07: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
Brendan Creane d2af6a4d39 net/dns/publicdns: don't upgrade Control D port-53-only addresses to DoH (#20463)
DoHEndpointFromIP mapped the entire 2606:1a40::/48 range to a
dns.controld.com/<id> DoH URL, but the ID-encoded addresses in that range
are legacy plaintext-DNS endpoints that refuse :443. They now fall through
as ordinary port-53 resolvers; the free anycast freedns.controld.com/pN
addresses still upgrade via exact match.

Fixes #20433

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-07-17 15:33:17 -07:00
Brendan Creane bab3f5fce7 wgengine/router/osrouter: remove orphaned tailnet addrs on cleanup (#20304)
* net/tsaddr: unmap IPv4-mapped IPv6 addrs in IsTailscaleIP

IsTailscaleIP branched on ip.Is4() before checking the CGNAT range, so an
IPv4-mapped IPv6 address (e.g. ::ffff:100.64.0.1) took the IPv6 path and was
tested only against the ULA range, wrongly returning false for a Tailscale
CGNAT address. Unmap at the top so both forms are classified identically;
Unmap is cheap and IsTailscaleIPv4 stays IPv4-only for callers that need it.

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

* wgengine/router/osrouter: remove orphaned tailnet addrs on cleanup

The orphan-address sweep added in #20199 ran only inside Router.Set, so the
teardown path (tailscaled --cleanup, and the unconditional cleanup at daemon
start) never removed stale Tailscale addresses a previous instance left on a
persistent tailscale0 -- it only flushed iptables/nftables.

Wire address removal into cleanUp: with no desired config, every Tailscale-range
address on the interface is an orphan, so enumerate and delete them all (IPv4
and IPv6, best-effort) in removeOrphanedAddrsForCleanup.

tailscaleInterfaceAddrs now yields the interface's addresses as an
iter.Seq[netip.Prefix], and the filters compose lazily over it: tailscaleAddrs
(every Tailscale-range address; used by cleanup), deletableAddrs (isDeletableAddr:
Tailscale-range and deletable now, i.e. excluding v6 when v6 is unavailable; used
by the live Set sweep), and orphanedAddrs (drops the desired addresses). The Set
sweep ranges the composed iterator directly, so no throwaway slices are built.
delAddress is made idempotent: it attempts both the loopback-rule teardown and
the address delete and joins their errors, so a missing firewall rule can't leak
the address, and it no longer no-ops on v6 (cleanup relies on that to remove v6
orphans even when this process never brought IPv6 up).

The Set-time sweep is otherwise unchanged; re-running it on network changes
(netmon) for late orphans remains a follow-up (tailscale/corp#43882).

Updates #19974
Fixes tailscale/corp#44173

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

---------

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-07-17 14:18:09 -07:00
Claus Lensbøl 9175fe2675 net/tstun: drop TSMP messages injected into TUN (#20511)
Enforce that TSMP messages are only accepted for transmission over the
wireguard connection from within the client.

Updates tailscale/corp#45059

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-07-17 15:10:08 -04:00
Claus Lensbøl 82a381e54b control/controlclient,net/tstun,wgengine/magicsock: fix handling of zero keys in TSMP (#20508)
Updates tailscale/corp#45042

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-07-17 14:02:53 -04:00
Jordan Whited cc0b3ddbbe net/packet: add TSMPType docs
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-07-17 09:56:25 -07:00
Jordan Whited 3076698cdf net/packet: fix TSMPType docs
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-07-17 09:28:49 -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 87c0d36942 net/routemanager,ipn/ipnlocal: name the changed-allowed-IPs map type
The map[key.NodePublic][]netip.Prefix that flows from route manager
commits to WireGuard device syncs has subtle semantics (a nil value
means the peer was removed or no longer contributes any prefixes)
that were documented on routemanager.Result.AllowedIPs and then
re-documented, or not, at each signature that passed it along. Give
it a named type, PeersWithRouteChanges, and document the nil
semantics once on it.

Updates #12542

Change-Id: I2566361a5331eb11b2b70a5bcdb497cc20ee561d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-14 19:57:59 -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
Brad Fitzpatrick 7e609b2581 ipn/ipnlocal,net/dns/resolver: serve MagicDNS names from live indexes
Every netmap change, including an incremental delta of a single peer,
rebuilt the full MagicDNS state twice: dnsConfigForNetmap walked all
peers to build the dns.Config.Hosts map, and resolver.SetConfig then
walked that map again to build its reverse (PTR) index. On a tailnet
with 10k peers that is a lot of garbage per delta.

Instead, add a resolver.MagicDNSHosts hook, installed once by
LocalBackend, that the quad-100 resolver consults on demand at query
time. It is backed by nodeBackend's nodeByName, nodeByAddr, and peers
indexes, which are already maintained incrementally as netmap deltas
arrive. The subdomain-resolve capability check also moves to the hook
(checking the node's CapMap at query time), so dns.Config's
SubdomainHosts is no longer populated.

dns.Config.Hosts remains for control's DNS.ExtraRecords, which are
few and which feed the split-DNS decisions in dns.Manager's
compileConfig, and on Windows it still carries every node's records
because the hosts-file fallback path (compileHostEntries) needs the
complete enumerable set. Those compileConfig decisions also consulted
the per-node Hosts entries (hasHostsWithoutSplitDNSRoutes), so a new
Config.MagicDNSHostsUnrouted bit preserves that signal now that node
records are not listed: with MagicDNS names present but MagicDNS
domain routing off, quad-100 stays in the OS resolver path.

One small behavior change: reverse (PTR) lookups now also answer for
node addresses whose forward records are filtered out by the
IPv6-suppression rule (issue #1152), since nodeByAddr indexes all node
addresses. Previously such addresses were absent from the pushed
Hosts map and thus from the reverse index.

Updates #12542
Updates tailscale/corp#43949

Change-Id: I63b99199c2b3b124c08cb8bbaea1f63165095294
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-14 07:26:41 -07:00
Brendan Creane 8eb18d902e net/dns/publicdns: use Control D anycast IPs for premium DoH (#20434)
For dns.controld.com premium resolvers we synthesized per-resolver IPv6
addresses by encoding the resolver ID into the 2606:1a40::/48 range, but
those are legacy plaintext-DNS (port 53) endpoints that refuse TCP :443.
DoH now dials Control D's shared anycast IPs (the resolver ID stays in the
URL path), fixing SERVFAIL on IPv6-only/NAT64 networks where the v4
anycast fallback isn't reachable.

Fixes #20430

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-07-13 16:47:23 -07:00
Brad Fitzpatrick 55b1a4de74 net/netcheck: don't mutate Client.TimeNow in AddReportHistoryForTest
AddReportHistoryForTest temporarily swapped out Client.TimeNow without
holding any lock, racing with concurrent GetReport calls reading it
from ReSTUN goroutines during tests. Instead, pass the current time to
addReportHistoryAndSetPreferredDERP explicitly so the test helper never
needs to touch the field.

Fixes #20438

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I9c4f0a2f9427b5f1d3e8b06a49f0d2b71c3ee8a4
2026-07-13 15:20:36 -07: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
Adriano Sela Aviles d69bf2685a all: apply go fix
Updates #cleanup

Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
2026-07-10 17:39:16 -07:00
Brad Fitzpatrick a5102d3fcb net/routemanager: add incremental route manager
Add a new RouteManager type that tracks per-peer self addresses and
advertised routes and incrementally maintains two read-only
snapshots: an IP-to-outbound-peer bart table carrying the per-peer
attributes the data plane needs (jailed state, masquerade addresses),
and a coarsened OS route set (including OneCGNAT consolidation).

Mutations are staged in a transaction (Begin/Commit) and applied to
the snapshots via bart's Persist methods, which path-copy only the
few trie nodes along the affected prefix, so a single-peer delta
costs a bounded amount of work independent of the number of peers,
instead of the O(n) full-world rebuild done today. Snapshots are
published via atomic pointer swap for lock-free reads on the hot
path, and Commit reports which peers' allowed IPs changed so callers
can sync wireguard-go incrementally. This is the same immutable value
snapshot pattern as used in the recent containerboot change,
364b952d62.

Nothing uses it yet; this is pulled out of a future change that wires
it into ipnlocal and wgengine, to make that PR smaller.

Updates #12542

Change-Id: Iccc5258024e6f90311835b79fd2d83b2adb0d09d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-10 16:57:09 -07:00
Aaron Klotz 2b62cb54a7 net/dns, util/winutil: improve detection of group policy affecting NRPT
Due to a customer issue, I investigated the Windows Dnscache service more
intensively. I learned that the only time it attempts to read the NRPT
from group policy is in response to a group policy change notification.

Under the hypothesis that policy refresh is not effectively delivering GP
notifications due to its dependency on reaching a DC, I replaced our use
of the RefreshPolicyEx with the quasi-documented GenerateGPNotification API.

Tests have been updated to ensure they check that they are running as
LocalSystem, which is required for GenerateGPNotification.

Fixes #20187

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2026-07-10 13:53:26 -06: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
Alex Chan b838d5caf7 all: fix typos where we repeat repeat ourselves
Found with the regex `\b([A-Za-z]+) \1\b`.

Updates #cleanup

Change-Id: I4cc51784d9b6437d3d0c66b531828707f87f7fd5
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-07-03 16:09:03 +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
Simon Law cb7e536804 feature/routecheck,ipn/routecheck: probe reachability in the background
Previously, refreshing the routecheck.Client would probe to generate a
new routecheck.Report, but this method was only wired up to the
LocalAPI and the `tailscale routecheck` command. However, waiting for
a probe to finish before choosing a router would take too long, so we
must keep a regularly updated report to be consulted as necessary.

This patch adds a Start and Close method to the routecheck.Client and
starts it in the background from features/routecheck. To enable this
feature for a given node, set both of the following node attributes:
`client-side-reachability` and `client-side-reachability-routecheck`.

This patch also wires up the RouterTracker.OnRoutersChange hook, which
fires a callback whenever a new network map includes information about
a router node, This signals to the routecheck.Client that it might
need to schedule another probe, if the shape of the routing table has
changed materially.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-02 20:26:27 -07:00
Simon LawandBrad Fitzpatrick d8ee47d1cf ipn/ipnext, feature/routecheck: subscribe routecheck to the IPN bus
This patch adds a new ipnext.NotifyWatcher interface that exposes
ipn.LocalBackend.WatchNotifications so that extensions inside
tailscaled can subscribe to the IPN bus, much like how the GUI
clients subscribe to it through the Local API.

This interface is used by the new feature/routecheck.RouterTracker to
watch for changes in the peer map that affect routers. RouterTracker
uses dead reckoning to incrementally maintain the set of routers. We
do this to avoid looping over the peer map repeatedly. See #17366.

RouterTracker supports two hooks:

- OnNetMapAvailable signals that the initial netmap has been received,
  so that the routecheck.Client can wake up goroutines that are
  waiting for it.

- OnRoutersChange signals that the set of routers has changed, so that
  the routecheck.Client can decide to probe a subset of the routers
  instead of all of them. Currently, this optimization hasn’t been
  implemented yet.

Updates #17366
Updates #20062
Updates tailscale/corp#33033

Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-02 20:26:27 -07:00
Simon Law 7b2432abae net/routecheck: tweak routecheck documentation and coding style
This patch is a follow-up for PR #19639 that does some cleanups.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-02 20:26:27 -07:00
Maisem Ali 9013b6ec1b net/dns: simplify split DNS compile path
Cache the OS split-DNS capability while compiling DNS config and return directly for split-capable platforms that do not need the Apple base-config workaround.

This removes the base-config sentinel pointer and keeps the iOS and sandboxed macOS fallback path explicit.

Updates #1338

Change-Id: I836417c8fa775b35d3be9bc80cf6841d30cec222

Signed-off-by: Maisem Ali <maisem@bold.dev>
2026-07-02 12:21:54 -07:00
Maisem Ali 10672a63f4 net/dns: support global resolvers in macOS tailscaled
The macOS tailscaled DNS configurator only wrote /etc/resolver files, which can express split DNS but not a primary resolver. Teach it to configure a global resolver through the SystemConfiguration dynamic store using scutil when OSConfig has nameservers and no match domains.

Let non-sandboxed macOS tailscaled follow Linux split DNS behavior when its OS configurator reports split DNS support. This avoids synthesizing an upstream default route from the machine's base DNS when the netmap did not provide one. Keep iOS and sandboxed macOS app builds on the existing Apple base-config path because those use NetworkExtension DNS settings rather than tailscaled's /etc/resolver configurator.

Add tests for switching between split and global DNS, including cleanup of stale Tailscale-managed resolver files, removal of the dynamic-store global DNS key, and preservation of the sandboxed macOS behavior.

RELNOTE: tailscaled on macOS now supports configuring global DNS resolvers.

Updates #1338

Change-Id: I9b2b61f89750a5529fc0add1cd37b1b9a355db12

Signed-off-by: Maisem Ali <maisem@bold.dev>
2026-07-02 12:21:54 -07:00