Commit Graph
301 Commits
Author SHA1 Message Date
Naman Sood 07915ae481 types/appctype: remove Conn25Attr type
This type was previously logically distinct from AppConnectorAttr
despite containing the same fields. It is now logically also the same,
so doesn't need to exist separately.

Fixes tailscale/corp#50897.

Signed-off-by: Naman Sood <mail@nsood.in>
2026-11-03 14:54:57 -05:00
Naman Sood d8eae46481 appc,feature/conn25,tailcfg: read conn25 config from non-experimental nodeattr
Updates tailscale/corp#39984.

Signed-off-by: Naman Sood <mail@nsood.in>
2026-11-03 12:30:26 -05:00
Mazdak Nasab 35d8212746 feature/conn25: block destinations inside the magic and transit IP pools (#22147)
Similar to b6fc02d for private destinations, a destination inside one of
the configured magic or transit IP pools is never somewhere an app can be
reached: the magic range is installed as a route towards Tailscale on
every configured node, and the transit range belongs to conn25's own
peer-to-connector mappings. Forwarding to one routes back into the
mapping layer instead of out to an app.

Updates https://github.com/tailscale/corp/issues/50118
Updates https://github.com/tailscale/corp/issues/46169

Change-Id: I8116543d4b7a02e1f4c3c6b882ee3ddec1ac4f97

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-11-02 14:30:42 -07:00
Fran Bull 0a88fa1ec6 feature/conn25: mint/verify TransitIPRequest tokens
Add functions to allow minting and verifying the tokens.

Updates tailscale/corp#46169

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-11-02 13:23:31 -08:00
Michael Ben-Ami 49aa9c859c feature/conn25: store tokens from DNS response and pass them back in
TransitIPRequest to connector

Parse the tokens from the HTTP headers in the response. Determine which
node responded to the DNS request from the RequestURL passed in as
metadata to (*extension).mapDNSResponse. Store the token and connector
node alongside the TransitIP in the addrs struct. Then populate the new
Token field in TransitIPRequest with the stored token.

Updates tailscale/corp#46169

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-10-31 15:22:34 -04:00
Fran Bull eb298524ee feature/conn25: create a secret for TransitIPRequest tokens
Create a secret that can be used for token minting and verification.

Updates tailscale/corp#46169

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-10-30 09:36:17 -07:00
Mazdak Nasab b6fc02d501 feature/conn25: block private destinations (#22073)
Similar to 7304dd4 for host-scoped destinations, conn25 needs to
block private destinations: a connector forwards to app destinations,
and the packet filter only ever sees the transit IP, never the
destination, so a private destination would let a client reach into the
connector's own network.

Updates https://github.com/tailscale/corp/issues/50118
Updates https://github.com/tailscale/corp/issues/46169

Change-Id: I700b0b5a270723f5a77907e35c52d729f88c44b6

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-30 08:55:05 -07:00
Adrian Dewhurst 7304dd42c1 feature/conn25, ipn/ipnlocal: use the new net/routelimits package for blocking host-scoped destinations
Similar to #20866 for 4via6, conn25 needs to block host-scoped
destinations. Due to the similarity between the two situations, the
matching is no longer in a 4via6-specific location. Since there is no
backwards compatibility to satisfy, there is no exception list for
conn25.

ipn/ipnlocal (for 4via6) and feature/conn25 now both use the new
package.

Updates tailscale/corp#50118
Updates tailscale/corp#46169

Change-Id: Ic3f9cccd2f5823a2ca48bd78607792b0fd6d7db9
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2026-10-29 13:57:37 -04:00
Mazdak Nasab 3ad81d1908 feature/conn25: return pool addresses when reserveAddresses fails
Addresses handed out by ippool.next() are marked in use, and the only path
back to the pool is the popExpired loop in reserveAddresses, which can
only see an addrs once it's in the client's assignments. So a reservation
that fails after taking an address leaks it from the pool
for the lifetime of the process.

The leak is the result of interaction between the magic and transit pools.
First an address from magic pool is picked. Then another address from
the transit pool is picked. If an error happens during picking an address
from transit pool, the address from the magic pool needs to be returned,
otherwise that address is leaked, as the address in not in the client
assignments and won't be released.

Fixes https://github.com/tailscale/tailscale/pull/22019

Change-Id: I3d98282762576cc57c135761859902a1d0a63c31
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-29 09:10:47 -07:00
Michael Ben-Ami de7245d25b feature/conn25,net/dns{,/resolver}: thread PeerAPI DoH metadata in DNS
response when DNS request goes to PeerAPI DoH

To support feature/conn25 and other extensions or consumers needing to
know which PeerAPI node handled a DNS request, e.g. for app connectors
or exit nodes, and needing extra data from the server that can be
transmitted in the headers.

DNS responses from other upstream resolvers do not get any metadata
populated, even if those servers are DoH.

Add a new Response type in net/dns/resolver that includes the raw
response bytes and the optional metadata, and replace the bare bytes
with it in some places.

Pass this new type to to net/dns.ResponseMapper instead of the bare
bytes to the extensions can see the metadata.

Updates tailscale/corp#46169

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-10-27 15:25:12 -04:00
Mazdak Nasab ce20d24a2b feature/conn25: add test coverage for DNS optional sections (#21782)
Additionals and authorities are two optional sections
in a DNS response. Conn25 treats DNS responses differently,
based on the response type. Adding test coverage for these
optional sections to assert the expected behavior.

Updates #cleanup

Change-Id: I778af5b368f53f6209fe2934aa98d9009c7fa395
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-26 16:44:37 -07:00
Brendan Creane a0a79982d4 feature/acme,tstest/acmetest: report the authorization error when an ACME order fails (#21790)
When validation fails, WaitOrder returns only the order's status, so
tailscale cert printed "acme: order ... status: invalid" with no reason.
The CA's reason is on the authorization's challenge. Wait on each
authorization before the order and return that error instead.

To test this, move natlab's fake ACME CA to tstest/acmetest, where unit
tests can import it, and teach it tls-alpn-01, a validation hook, and
the "invalid" state a CA reports when validation fails.

Updates #21759

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-10-15 07:45:30 -07:00
James Tucker fb79793d99 net/tstun,feature/conn25: test inbound replies against a full injection queue
Add regression guards for the inbound reply paths (TSMP pong, TSMP
connection reject, and the conn25 unmapped Transit IP reject) that were
made nonblocking via TryInjectOutbound: with the injection queue full
and its reader not draining (as when the reader is blocked behind
wireguard-go's device.peers lock held by RemovePeer), generating a
reply must drop rather than block, because the replying goroutine
must complete for the peer teardown to finish. A blocking injection
there deadlocks peer removal against inbound packet processing, as in
tailscale/corp#45544.

The tests run in synctest bubbles: the full queue is the only blocking
operation on these paths, so after synctest.Wait the reply either
completed or is provably stuck on the queue, and no timeout-based
failure is needed. A regressed, blocking sender is unblocked by
closing the Wrapper before the test fails, so the bubble settles.

Updates tailscale/corp#45544
Updates #21631

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-13 15:55:43 -07:00
James Tucker c606e83fb2 net/tstun,feature/conn25: make ErrInjectionQueueFull detectable as ENOBUFS
Export ErrInjectionQueueFull, returned by TryInjectOutbound when a full
injection queue forced a packet drop, and define it to wrap
syscall.ENOBUFS on platforms that define that errno. Callers can now
test for buffer exhaustion generically with errors.Is(err,
syscall.ENOBUFS) instead of knowing about tstun's error type. plan9 and
tamago have no ENOBUFS in their syscall packages, so there the error is
a plain sentinel; code that must build on those platforms (or on all
platforms, like feature/conn25) can still match the exported sentinel.

Also stop conn25 from logging queue-full TSMP rejections: those drops
are expected under saturation and are already counted as reason="queue_full"
in the outbound usermetric.

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-13 15:55:43 -07:00
James Tucker 3c0508fa28 net/tstun,wgengine/netstack: avoid deadlocks on full outbound queues
Make link queue writes nonblocking: gVisor can generate replies on the
reader needed to drain the queue. A full queue drops only the rejected
packet and reports ErrNoBufferSpace, which gVisor counts and TCP handles
by arming its retransmit timer. UDP sockets do not see the error unless
IP_RECVERR is set, matching Linux. Count WireGuard-bound drops as
reason="queue_full" in tailscaled_outbound_dropped_packets_total, and
per-queue drops in debug and client metrics.

Add Wrapper.TryInjectOutbound for best-effort replies generated while
processing packets (TSMP rejects and pongs, conn25 rejects, fake-mode
echo replies), so those paths drop instead of stalling behind the
injection queue's reader. Its drops are also counted as queue_full.

Make outbound injection waits cancellable and close link queues before
abort-generated replies. Stop the WireGuard injection pump on ErrClosed.
Release injected packets still queued at Close, outside closeOnce to
allow reentrant release callbacks; a sender whose send races with Close
releases the packet itself. Release temporary capture buffers.

Copy injected payload views directly into the WireGuard slab, avoiding
an intermediate allocation and copy.

Test queue saturation, multiqueue progress, cancellation, concurrent
shutdown, reference ownership, fragmented payloads, and GSO.

Performance versus the parent commit, on a 4-core Xeon 6975P-C:

  inject+read 64B / 1280B       245ns / 479ns -> 187ns / 195ns, 0 allocs
  4 injectors, 1 reader         843ns -> 451ns
  64KiB GSO inject+split        10.9us -> 4.4us, 64KiB/op -> 0
  link queue write+read         81.4ns -> 81.5ns
  tsnet TCP, 1 / 8 streams      448 / 580 MB/s -> 492 / 674 MB/s

BenchmarkTCPQueueBottleneck shows that dropping on full queues keeps
throughput at the bottleneck rate (117 MB/s at 100k packets/s, as with
blocking writes), at the cost of more retransmits with many streams.

Fixes #21631
Updates #21638
Updates #21632
Updates #21639

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-13 15:55:43 -07:00
Mazdak Nasab aaaaa97189 feature/conn25: lock mu in lookupTransitIPsByConnKey
Lock mutex in lookupTransitIPsByConnKey right
before critical section, rather than in the caller.

Updates #cleanup

Change-Id: I8ee8950dce056e1945d4356d82f0bffbc6688624
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-13 13:26:43 -07:00
Brendan Creane 3bb8a906c2 feature/acme: only refresh certs for HTTPS serve entries (#21736)
The cert refresh loop treated every Web entry in the ServeConfig as
HTTPS, so a node serving only plain HTTP fetched and renewed a Let's
Encrypt cert for its name. Check the port's TCP handler for HTTPS
before considering a Web entry, for node and Service entries alike.

Fixes #21693

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-10-10 09:53:11 -07:00
Mazdak Nasab 5b226c7d1d feature/conn25: write through email and voip related DNS responses
Write through email related DNS responses (MX and TXT),
and voip ones (SRV and NAPTR) as we try to not break them.

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

Change-Id: I920c4e4401f5a20bcdcad092e94fce8ffa0f12b5
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-09 14:00:58 -07:00
Alex Valiushko 76648a00fd net/tstun: implement independent injection queue (#21681)
Injected packets now arrive on a queue of their own rather than being
multiplexed onto queue 0. Wrapper.Read is deprecated. It still reads the
first kernel-backed queue, but from now on no single queue of a Wrapper
sees every packet.

Deletes the poll goroutine, the bufferConsumed token, the vectorOutbound
channel, tunVectorReadResult, a full-size slab, and the copy of every
accepted packet between the two slabs.

wireguard-go now runs an extra TUN reader goroutine, a net wash compared
to running our poll routine.

Measured on c8gn.2xlarge, Ubuntu 24.04, Kernel v6.14, tailscaled under
load from two peers, each running iperf3 -P8.

            tx mean   rx mean   cores
  main       7.00      8.84      3.49
  branch     8.94      8.86      3.95

Bumps wireguard-go pin to tailscale/wireguard-go@e61b6b7811.

Updates tailscale/corp#37878

Change-Id: Ide0e8a9f345371b2747fad59dd2bf963c8af5b63

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-10-08 13:13:11 -07:00
Mazdak Nasab 417d3c00b0 feature/conn25: lock mutex right before shared resources in client
Lock shared resources right before access
for clarity and preventing bugs.

Updates #cleanup

Change-Id: I1f905a3e4ec8b9ede4e8846e84e7bc50d78c67f0
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-08 09:26:56 -07:00
Mazdak Nasab 3420967a5f feature/conn25: lock mutext in client resendTransitIPMapping
Lock mutext in resendTransitIPMapping before accessing
the shared resource to prevent data race.

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

Change-Id: I35b9e99428361732c9a785020d7609d29a99877e
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-10-08 09:26:56 -07:00
James Tucker 852c3d1f0a feature/exitnodehealth: refine exit node warnings based on responsiveness
Report when an active selected exit node has not answered disco messages for a minute. Include the node name and last response time in the warning.

Evaluate responsiveness from existing magicsock heartbeats and clear the warning when the peer becomes idle or responds. Use changes to nonzero WireGuard RX and TX counters to suppress warnings when traffic is working. Keep observation history and warning evaluation in the exitnodehealth feature. WireGuard-only peers are excluded.

Updates #tailscale/corp#33007

Signed-off-by: James Tucker <james@tailscale.com>
2026-10-07 15:38:22 -07:00
Brad Fitzpatrick 92a64b306d ipn/{ipnext,ipnlocal},feature/conn25: add ExtraDNSRoutes hook for conn25 split DNS
dnsConfigForNetmap called appc.AppDNSRoutes directly to synthesize the
conn25 split DNS routes from the self node's capability map, which was
the one place node_backend.go depended on the appc package and was
marked with a TODO to make it a hook.

Add ipnext.Hooks.ExtraDNSRoutes, which returns extension-supplied split
DNS routes. authReconfigLocked calls it and passes the result into
dnsConfigForNetmap, which adds the routes alongside the netmap's own
routes with the same UseWithExitNode handling as before.

The hook takes no arguments: the conn25 extension computes the routes
in its OnSelfChange handler and clears them on a switch to a different
node, and it already tracks the AppConnector.Advertise pref from
ProfileStateChange. Both of those hooks fire before the reconfig that
consults ExtraDNSRoutes, so the state is current. The conditions are
unchanged: no routes when this node itself advertises as an app
connector, and none without the experimental capability.

Updates tailscale/corp#37125

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I6c2e9f4b8a1d3e7f5c0b2a9d8e6f4c3b1a7d5e9f
2026-10-05 10:14:23 -07: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
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
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
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
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
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
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
Mazdak Nasab 72fa821ed4 feature/conn25: clean up formatting in conn25*.go files
Improving formatting in conn25.go and conn25_test.go.

Updates #cleanup

Change-Id: Ic597e8e3e9284d0cc244de538abd6a563f783f32
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-25 09:46:53 -07:00
Mazdak Nasab 5af313f0aa feature/conn25: validate ConnectorTransitIPRequest contains TransitIPs in the pool
Validate ConnectorTransitIPRequest messages contain
TransitIPs in the configured pool.

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

Change-Id: I35dfcaa95a08b4347b1164df905cc59de47e4d96
Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-25 09:46:53 -07:00
Mazdak Nasab b6c6c59dde feature/conn25: extend datapath test to verify full packets (#21422)
Extend datapath tests to not only assert headers but
also the full expected packets.

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

Change-Id: I0e9a1c2f61f2d63eafa430d8e6436908fe927cf4

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-23 13:52:21 -07:00
Brad Fitzpatrick 0931824b5f feature/androiddns: fall back to getaddrinfo on Android 9 and older
The resnsend dnsproxyd command this package relays raw DNS messages
through was added in Android 10. On Android 9 and older the daemon
answers it with FrameworkListener's text "500 Command not recognized",
which we read as binary: "500 " passed the negative errno check and
"Comm" became the answer length, so every lookup failed with
"androiddns: bogus answer length 1131375981". That's what a tailcat
user hit on a Fire TV stick, which runs Android 9 under Fire OS 7.

Detect that text reply (a binary reply never starts with an ASCII
digit) and switch the process over to the daemon's older getaddrinfo
command, the one bionic's getaddrinfo proxies through. Parse the
single A or AAAA question out of the query, send the command with the
matching address family, and synthesize a DNS answer from the addrinfo
list that comes back, mapping EAI_NONAME to NXDOMAIN and EAI_NODATA to
an empty answer so Go's resolver produces its usual errors. Other
query types return an error saying the Android version can't answer
them.

The reply layout is the field-by-field one netd has used since
Android 6.0 (a 64-bit netd may serve a 32-bit client, so it stopped
sending the raw struct); Android 5.x's raw struct layout is detected
and rejected rather than guessed at. The format was verified against a
32-bit Fire OS 7.7.1.3 device (Android 9, API 28), where a tailcat
build with this change resolves names, fetches its DERP map over
HTTPS, and accepts a connection from another machine.

Updates tailscale/tailcat#126

Change-Id: I7062984c7ec8ce6caf737089e220ef2ca439dcc5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-23 10:45:09 -07:00
Mazdak Nasab 1edf391c9c feature/conn25: add ipv6 test coverage for datapath (#21362)
Add ipv6 test coverage for conn25 datapath by
extending existing tests.

Fixes tailscale/corp#42521

Change-Id: I4c6df35572d96cc80cae3685353522942e2a8546

Signed-off-by: Mazdak Nasab <mazdak.nasab@gmail.com>
2026-09-22 09:36:50 -07:00
Patrick O'Doherty 2e72593fbf feature/clientupdate: require write access for update/install localapi (#21360)
The update/install localapi handler had no PermitWrite check, so any
local user who could reach the localapi socket could make the root
daemon self-update and restart itself. This has been the case since the
endpoint was added in November 2023.

Gate the handler behind PermitWrite so that only root or the operator
user can trigger a self-update, matching the other mutating handlers.

Updates tailscale/corp#48187

Change-Id: Iadfef939f5dab684652cd220e77de63b54bfca2f
Reported-by: Ben Carman <benthecarman@live.com>

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2026-09-17 14:30:40 -07:00
Michael Ben-Ami b5e07cbf53 feature/conn25,types/appctype: delete TemporaryUnsafeBypassFilter from
conn25 node attribute type

This node attribute configuration knob, when enabled, was bypassing two
peercap (tailscale.com/cap/conn25/<app_name>) checks while they were
under development and testing:

- PeerAPI DoH on the connector verifying the peercap includes the app in
  query parameter of the request.
- PeerAPI ConnectorTransitIPRequest verifying the peercap includes the
  app in the request.

With this change, we always perform those checks, and delete bypass
knob.

Updates tailscale/corp#40076
Updates tailscale/corp#48041

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-09-15 15:34:05 -04:00
Brad Fitzpatrick 2ee809d10d feature: add TS_DISABLE_FEATURE to disable features at runtime
The ts_omit_<name> build tags omit a feature at build time; there has
been no way to do the same at runtime. Some users (either proactively
or in response to a security announcement) might like a way to disable
a feature that's linked-in in their binaries that they're not using.
Then a mitigation announcement can say "set this env var" without
asking users to rebuild or wait for a new release.

This adds env var TS_DISABLE_FEATURE, a comma-separated list of
feature names to disable, and the listed set is reported by the
debug-optional-features LocalAPI endpoint next to the registered set.
The legacy per-feature knobs such as TS_DISABLE_SSH_SERVER and
TS_DISABLE_TAILDROP keep working independently.

A disabled feature behaves as if it had not been linked: it is absent
from feature.IsRegistered, its hooks are unset, and its extensions and
handlers are not registered. Three pieces make that happen:

  * feature.Register now returns bool, false when disabled, and
    feature packages gate their registration init on it. It was added
    to the feature packages that never called it (including taildrop
    and ssh), which also completes the picture reported by
    debug-optional-features. taildrop, routecheck, favorites, and
    serviceclientprefs had registration split across several inits and
    now register from one gated init.
  * ipnext.RegisterExtension ignores a disabled feature's extension.
  * feature.Hook.Set and feature.Hooks.Add walk the call stack and
    silently skip when the calling package under feature/<name> is
    disabled. This covers sub-packages such as
    feature/captiveportal/netcheckhook, which cannot call Register
    themselves without colliding with their parent, and future
    packages whose authors forget the gate.

ssh/tailssh's registrations moved from its inits into tailssh.Register,
called from feature/ssh's gated init. The aws and kube state stores and
syspolicy's Windows store registration are gated too.

feature/register_disable_test.go runs this test binary as a child
process (it links condregister, as tailscaled does) with
TS_DISABLE_FEATURE set to every registered feature at once, and fails
if any of them register anyway, so a feature that ignores the variable
cannot land.

Updates #12614

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I720af6ccab844ae060a9dfd1539fee577fd483e3
2026-09-15 08:37:47 -07:00
JeremiahM37 343e2b2817 portlist: use inet_diag for Linux listener discovery
Query listening TCP and unconnected UDP sockets through inet_diag instead
of scanning the proc socket tables. Preserve process attribution and fall
back to proc on unsupported, interrupted, or invalid diagnostic dumps.
Cache missing protocol handlers as unsupported to avoid retrying every poll.

Read terminal dump flags directly: the pinned netlink library removes
NLMSG_DONE without checking NLM_F_DUMP_INTR. Keep socket creation and
recovery consistent with the proc namespace and close the poller when
its service discovery loop exits.

Include parser, request encoding, and errno classification tests.

Updates #10430
Updates #15492

Signed-off-by: JeremiahM37 <JeremiahM37@users.noreply.github.com>
2026-09-14 19:16:36 -07:00
Brad Fitzpatrick 023255e8a2 net/dnscache, feature/dnsresolvecache: only persist DNS resolutions after TLS verification
This is a follow-up to #21029 (aa2681ac5f) to make it a bit stricter
and not cache DNS results until they've passed TLS cert validation,
to weed out DNS servers that are lying (like captive portals).

Because this is done via dnscache.TLSDialer we only catch the control
connection, but that's fine. That's all we need to come back alive
if DNS was down because real system DNS is itself over Tailscale.
The DERP connections should come via IPv4/IPv6 fields in the DERPMap.
And the logging connection isn't important; it'll buffer and catch up
later as needed when DNS is back up.

Updates #21028

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I75f176a0222f04ed52c9de1247deeed9b911f92c
2026-09-10 12:49:19 -07:00
James Tucker 85c1efb466 net/{connreject,tstun},feature/connreject,wgengine,client/local: add connection-rejection diagnostics
Adds an opt-in, in-memory aggregator of recent connection-rejection
events (TSMP rejects received from peers, outbound TSMP rejects we emit
on ACL-blocked inbound flows, and pendopen timeouts) keyed by
(direction, proto, peer-address, reason). The aggregated data is exposed
over a new debug-rejects LocalAPI endpoint and a GET /debug/rejects c2n
endpoint, intended for future GUI/CLI consumption when diagnosing why a
connection failed.

Architecture:
 - net/connreject holds the data types and a per-LocalBackend
   Aggregator (LRU-bounded, default 256 entries on desktop / 32 on
   mobile, per direction).
 - feature/connreject is a self-registering ipnext.Extension that owns
   one Aggregator per LocalBackend, installs note callbacks on the
   tundev and engine, subscribes to OnSelfChange to flip the runtime
   gate, and serves the LocalAPI/c2n endpoints.
 - wgengine.Engine and *tstun.Wrapper each gain a SetConnRejectNote
   setter; data-plane sites use a single atomic.Pointer load + nil
   check, so the cost when no consumer is installed is one MOV.

Gating:
 - Compile-time: ts_omit_connreject build tag (standard
   feature/buildfeatures + condregister plumbing). Trims ~41 KB.
 - Runtime: nodecap.ConnReject node attribute, off by default
   at the control plane. May be removed once the feature is enabled
   by default.

Updates CapabilityVersion to 146 (clients understand nodecap.ConnReject
and can serve GET /debug/rejects).

Adds Proto/Src/Dst accessors on flowtrack.Tuple (used by pendopen to
construct events without exposing the tuple's internals to the
aggregator).

Updates #1094
Updates #14802

Change-Id: I83e8f24a7e66fa2d158d128bd25fbe851134941b
Signed-off-by: James Tucker <james@tailscale.com>
2026-09-08 16:22:43 -07:00
Brad Fitzpatrick aa2681ac5f net/dnscache, feature/dnsresolvecache: persist DNS resolutions to disk
Add a new modular dnsresolvecache feature that records every successful
DNS resolution from net/dnscache as a JSON file per hostname in
$statedir/dns-cache/, so a later boot with misconfigured DNS can still
find last-known-good IPs for critical hostnames like the control plane.
Files are rewritten only when their contents change, so a file's
modification time records when the answer last changed.

When regular DNS resolution fails, the disk cache is now consulted
before the DERP-based bootstrap DNS in net/dnsfallback. This is the
first step toward removing the DERP-based mechanism: new clientmetrics
(dnscache_disk_fallback_hit, dnscache_disk_fallback_miss,
dnscache_derp_fallback_ok, dnscache_derp_fallback_dial_ok) will tell us
when the DERP path no longer fires in the fleet and can be deleted.

The feature is linked into tailscaled by default (omittable with
ts_omit_dnsresolvecache) and is not included in tsnet.

Updates #21028

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I59228cbf68e3b48dfb1215cdd12bd8166ab14034
2026-09-08 16:11:02 -07:00
Adrian Dewhurst a4c7902240 feature/conn25, ipn/ipnlocal: restrict conn25 DNS by peercap
Instead of opening all DNS queries with conn25, only permit clients to
access domains of the apps that they have permission to use. Previous
changes ensure that clients report which app they are making a DNS query
for, simplifying the check.

Updates tailscale/corp#40076
Updates tailscale/corp#47585

Change-Id: I40fee220b3f190b2a0db9b3e6cc79f323ef16b73
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2026-09-08 15:57:20 -04:00
Brad Fitzpatrick 60d9c54b6b feature/androidbin: make netmon and TLS work in raw binaries on Android
Android denies app UIDs both NETLINK_ROUTE (golang/go#40569, #2293)
and /proc/net, so net.Interfaces always fails and netmon.New errors
out before a standalone binary can do anything. The Android app solves
this from Java via netmon.RegisterInterfaceGetter, but raw binaries
run under Termux or a rooted shell have no Java to lean on. This is
the second half of #21129, following the androiddns feature.

I added a netmon fallback hook, consulted only when no interface
getter was registered and net.Interfaces failed, and a new androidbin
feature (ts_omit_androidbin) that implements it: report a single
synthetic interface whose v4 and v6 addresses come from asking the
kernel to route an outbound UDP socket, which sends no packets and is
permitted in the app sandbox. That's enough for magicsock to discover
local endpoints. The fallback also requires runtime evidence of
Android (GOOS=android, or /dev/__properties__ existing for GOOS=linux
binaries running under an Android kernel), so it's inert on regular
Linux.

The androidbin feature also blank imports androiddns, and fixes a
third gap I found while testing: GOOS=linux binaries on Android have
an empty CA root pool, because Go's unix root loader doesn't know
Android's /system/etc/security/cacerts (the GOOS=android loader
does), so all TLS verification fails. On Android it points
SSL_CERT_DIR there unless the user already set it, as Termux's
ca-certificates package does.

The feature is on by default in tailscaled builds on Linux and
Android via condregister, and deliberately not linked into tsnet by
default; tsnet apps and other programs opt in with a blank import of
tailscale.com/feature/androidbin.

I verified on an Android 13 emulator with SELinux enforcing, running
GOOS=linux static binaries under the app UID (run-as), where
net.Interfaces fails with the exact netlinkrib permission denial from
the issue: netmon.New succeeds with the synthetic interface, and a
tailcat binary importing this feature does DNS via dnsproxyd, fetches
its DERP map over TLS using the Android cert store, completes STUN,
selects a DERP region, and prints its address.

Updates #21129

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: If7dbcbb825ecd24bcf6d9d64b1e334dd00700d51
2026-09-08 11:41:54 -07:00
Brad Fitzpatrick 86b3cd5aa1 feature/androiddns: add dnsproxyd DNS resolution for standalone Android binaries
Android doesn't have /etc/resolv.conf. This causes problems for people
running GOOS=linux binaries (or GOOS=android binaries without cgo, so
they don't use Android's bionic libc) in Termux, adb shell, etc.
(Go binaries built with cgo use bionic on Android: golang/go#10714)

16 years ago when I was on the Android team I added a system-wide DNS
cache (dnsproxyd) and made bionic query that, so each Android app
wasn't doing its own DNS resolution. That interface was never meant to
be stable, and I thought that code would be surely dead by now 16
years later, but apparently it lives on, and is more stable now: both
empirically (time, ossification?), and because of how Android's split system
updates work nowadays, the dnsproxyd lives on the other side of bionic,
so they seem to keep it pretty stable. The old bionic<->dnsproxyd APIs
I added 16 years ago are still there, but 8 years ago it got some additional
APIs to query by a DNS packet instead.

So use it! If we find ourselves on Android and without libc access
(and because we don't want to pull in ebitengine/purego with all its
side effects), just query the DNS server like bionic does.

This can be disabled in Linux binaries with ts_omit_androiddns.

Old links:

  LineageOS/android_system_netd@007e987fee
  https://android.googlesource.com/platform/system/netd/+/007e987fee7e815e0c4bc820f434a632b7a69a9d
  ("DNS proxy thread in netd.")

  aosp-mirror/platform_bionic@a1dbf0b453
  https://android.googlesource.com/platform/bionic/+/a1dbf0b453801620565e5911f354f82706b0200d
  ("DNS proxy: the start.  proxies getaddrinfo calls.")

Back then I found it cleaner to proxy at the getaddrinfo level rather
than speak in terms of DNS packets. The raw-packet resnsend command I
use here came eight years later, added in November 2018 for Android
10's android_res_nsend NDK API:

  LineageOS/android_system_netd@c0c818f448
  https://android.googlesource.com/platform/system/netd/+/c0c818f448efa90ab1f9b1733fb86c5e22fb894c
  ("Add resNetworkSend cmd in DnsProxyListener")

Android 10 (codename Q, API level 29, released September 2019) is
therefore the minimum OS version for this to work.

I verified this against the DnsResolver module on an Android 13
emulator with SELinux enforcing, from the shell UID, with both a pure
Go GOOS=android binary and a static GOOS=linux binary: raw queries,
NXDOMAIN handling, the runtime Android detection, and a tailcat binary
reaching DERP with lookups visible in the daemon's logcat output, some
served from my 2010 DNS cache.

Updates #21129

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0d63763e255a077e4e5745b3e64ba0d78dab6d69
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-07 18:35:31 -07:00
Jordan Whited 8fc6dca15e all: use ~128KiB packet buf for batched i/o
This commit bumps the wireguard-go dependency to incorporate changes to
the packet memory model and the tun.Device.Read and conn.ReceiveFunc I/O
interfaces. It updates their implementations accordingly.

These changes improve throughput in all measured benchmarks and reduce
peak RSS in six of eight cases. The two regressions will be addressed in
a follow-up commit that reduces peak RSS below the baseline measured at
1e69418. That work is kept separate to simplify review.

The following throughput and peak RSS benchmarks were performed with
iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8).

The UDP benchmarks did not use UDP GSO on the sender, so they were
roughly equivalent to single packet I/O through wireguard-go.

TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP
streams.

Throughput (Mb/s)
  Test     1e69418  After  Change
  TCP/1    10,371  11,354   +9.5%
  TCP/128   7,886   8,404   +6.6%
  UDP/1     2,111   2,853  +35.1%
  UDP/128   1,747   2,235  +28.0%

Peak memory (VmHWM, kB)
  Test     Side   1e69418    After   Change
  TCP/1    TX      98,240   52,596   -46.5%
           RX     287,748   73,384   -74.5%
  TCP/128  TX     101,196   52,812   -47.8%
           RX     290,420   63,620   -78.1%
  UDP/1    TX      58,864  160,840  +173.2%
           RX     137,516   49,900   -63.7%
  UDP/128  TX      66,148  116,096   +75.5%
           RX     154,384   56,556   -63.4%

Updates tailscale/corp#46716
Updates tailscale/corp#22467
Updates tailscale/corp#36989
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-09-04 09:32:55 -07:00
Fran Bull 99f1ee74bb feature/conn25: use traffic steering to select connector
Use whatever traffic steering's notion of the best connector for the
client is, rather than picking arbitrarily.

Fixes tailscale/corp#46766

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-09-01 07:46:11 -07:00
Fran Bull a7769cbc33 feature/conn25: check online when picking connector
So that when control notices a node isn't responding and sends that info
down in a netmap we will stop picking that node as a connector. This
doesn't provide great failover (it takes several minutes) but it's
better than not failing over at all.

Updates tailscale/corp#47250

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-08-26 15:10:24 -07:00
Fran Bull 247c67809b appc,feature/conn25: move functions to where they are used
The functions used to be used in ipn/ipnlocal but we changed that, so
the functions can be moved into feature/conn25 now.

Updates tailscale/corp#47250

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-08-26 15:10:24 -07:00