Commit Graph
11230 Commits
Author SHA1 Message Date
Brendan Creane 8236b77e4e .github/workflows: cache the Go build and tsgo toolchain in natlab CI (#20815)
The natlab workflows cached only the VM images, so every job re-downloaded
the tsgo toolchain and built Go from scratch. Cache the toolchain, module
cache, and build cache in both workflows. natlab-test's prepare job is the
only writer of the build cache; everything else, including its 54 matrix
jobs and natlab-basic, restores it.

The warm step compiles with both toolchains on purpose: ./tool/go builds the
test binaries, while the bare `go` that vmtest.go and gokrazy/build shell out
to is the runner's stock Go. GOCACHE keys embed the compiler's build ID, so
those entries don't interchange, but both land in ~/.cache/go-build.

Updates #13038

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-09-03 12:15:41 -07:00
Brad Fitzpatrick 92ec102673 net/netcheck: mark address family sendable on STUN response
runProbe records IPv4CanSend or IPv6CanSend after SendPacket returns
successfully. A sufficiently fast STUN response can be received and
processed before that return, however, and the report can be cloned in
the intervening window. That produces a contradictory report with UDP
and a valid mapping, but CanSend false. Magicsock treats that as a
send failure and unnecessarily rebinds, perturbing tailcat tests.

A received STUN response itself proves that its address family sent
successfully, so also mark the family sendable while recording the
response.

Before this, the tailcat test flaked after ~3700 runs under
flakestress. Now it can run 30 minutes (12,203 successful runs).

Updates tailscale/tailcat#73

Change-Id: I54fe2e81fc703fac36f693e9dbf0c3fa27d119b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-03 10:15:01 -07:00
Claus Lensbøl f53c28101a control/controlclient,ipn/ipnlocal,wgengine/{,magicsock}: route disco keys learned over TSMP directly to magicsock (#21016)
Previously, the new disco keys entering the client via TSMP was routed
into controlClient to allow for deduplication and filtering of disco
keys and avoid control overwriting an active TSMP learned key with a
stale key.

A system supporting multiple disco keys was introduced to allow these
keys living side by side, along side a system for selecting an active
egress key with a bias towards keys learned via TSMP. On ingress any
known key is accepted.

This PR makes the switch to key routing, by sending new TSMP learned
disco keys directly into the userspace engine and in turn magicsock,
removing the need for a full layer of deduplication in the
controlClient.

Additionally, the system that previously fully reconfigured clients on
disco key updates coming from control is now using an optimistic
handshake througha newly introduced wireguard-go method,
ScheduleHandshakeOnUserSend implemented in:
https://github.com/tailscale/wireguard-go/pull/81

What this PR does not do is revert the mapSession back to being single
writer. Bringing the mapSession back to this state is desired, however
to ensure the plumbing is done right and easier to reason about, that
will be done in a separate PR.

Updates #20590

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-03 09:38:28 -04:00
Brad Fitzpatrick 6c06e00ad3 .github/workflows: add tailcat cross-repo breakage early warning tests
Add a workflow that checks out tailscale/tailcat alongside this repo and
runs its tests in a go.work workspace where tailscale.com resolves to the
tailscale.com repo at head instead of tailcat's pinned version. This gives
us an early warning when a change here breaks tailcat's API or behavior
expectations. Such a failure might be fine if the change is intentional
and coordinated with a tailcat fix, but the goal is to catch accidents.

Updates tailscale/corp#24454

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0566437ee86bd0403c708b2f44e7ef169abf2570
2026-09-02 16:01:18 -07:00
Mike Jensen 642099cf89 fuzz: hide external test files from go-118-fuzz-build during fuzzer builds (#21082)
go-118-fuzz-build (fc5dc53b) overlays every sibling _test.go in the fuzzer's directory onto a non-test path (<base>_libFuzzer.go). Sibling test files declaring an external test package (package foo_test) then collide with the fuzzer's package, failing the build with "found packages foo and foo_test".

One possible fix would be to stop using external _test package for our tests, but this change attempts to address this issue without changing our test packaging structure.

This change wraps each compile_native_go_fuzzer_v2 call in build_fuzzer, which hides the files in `go list .XTestGoFiles` for the build. Internal test files stay visible since the fuzz target may use their helpers.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: Ic6805854f76c597a5edff60674b5ea05b2d18ea9

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-02 11:21:32 -06:00
Jake Bailey 91d10d38ae net/portmapper: avoid leaking permanent UPnP mappings
MikroTik routers only support permanent UPnP leases. Previously, a
mapping attempt could proceed as follows:

  1. Add a temporary mapping.
  2. Retry with a permanent mapping after error 725.
  3. Successfully create the permanent mapping.
  4. Fail to query the external IP.
  5. Forget the mapping and retry with another random port.

Each retry left another permanent NAT rule behind.

To prevent this, query and validate the external IP before creating
the mapping. When service selection has already queried the address,
reuse that result. This leaves no fallible network request after
AddPortMapping succeeds.

I tested this on my own MikroTik router running RouterOS 7.23.2. A
successful mapping queried the external IP before creating the
permanent rule, and a forced external-IP failure created no rule.

Updates #10602

RELNOTE: Prevent UPnP rule accumulation on MikroTik routers.

Change-Id: Id925ed9cc3a3da6ecb06fc892f2a934addfac37b
Signed-off-by: Jake Bailey <jacob.b.bailey@gmail.com>
2026-09-01 20:07:27 -07:00
Brad Fitzpatrick b3c719581e go.toolchain.rev: bump to Go 1.27.1
Updates #bump

Change-Id: I5ad390397062bf9887962c34a8f3a047ea9586e9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-01 15:47:33 -07:00
Brad Fitzpatrick c3736ae41a Dockerfile: use our own Go toolchain, not the golang Docker image
The golang:N-alpine Docker image lags behind Go minor releases by a
day or two. When we bump go.mod to a new minor version before the
image catches up, the required "Build Docker image" CI check fails
with "go.mod requires go >= 1.27.1 (running go 1.27.0;
GOTOOLCHAIN=local)" and blocks the toolchain bump from merging.

Instead, base the build stage on plain alpine and download the
Tailscale Go toolchain release for the revision in go.toolchain.rev,
matching how everything else in this repo is built.

Fixes #21072

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I1ccb77fa3c7a49532ea87dbdbf9e3340880ec94e
2026-09-01 15:47:33 -07:00
Mike Jensen 0188b38666 fuzz: refactor fuzzing to new layout for oss-fuzz support (#21069)
Prior to this change there were two problems with our fuzzing for oss-fuzz:
  1. There was an issue if the fuzzing spanned two files (mingled with the testing).
  2. The fuzzing needs to be part of the implementation package (no _test packages).

This change fixes that by moving all package fuzzing into a common `fuzz_test.go` within the package.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: I0b95edcd0df946f723eea32f575c679214c0b202

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-01 13:15:45 -06:00
Gesa Stupperich 316b22b775 cmd/tsconnect{,wasmbuild}: allow passing version stamps via env
When the wasmbuild is run from external workflows, it can't derive
the version stamps from its build context. Allow setting the
ProdLDFlags version.longStamp and version.shortStamp from the
VERSION_LONG and VERSION_SHORT environment variables when present.

This way, if the wasmbuild caller already knows them (e.g. from
mkversion), it can pass them in.

This avoids "x.y.z-ERR-BuildInfo" showing up in the admin console's
machine version column.

Updates #19707

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
2026-09-01 17:30:01 +01:00
Claus Lensbøl e1d17a6b99 wgengine/magicsock: dynamically switch active disco key on recv (#20984)
When receiving disco traffic from a node, mark that node as having been
seen. Should that disco key not be the active use disco key, switch to
that one as being the active key. Additionally, clear states on the
magicsock connection and prepare for sending a new WG handshake whenever
user data is transmitted.

Sets up for:
 - Routing TSMP keys directly into magicsock
 - Switching the active connection reset mechanism to the optimistic
   handshake
 - Cleaning up paths into controlClient

Updates #20494

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-09-01 11:18:25 -04: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
Brad Fitzpatrick c542f61367 go.toolchain.rev: bump tailscale/go toolchain
From Go 1.27.0 to Go 1.27.0 + latest upstream release branches,
plus a cherry-pick of an x/net http2 hpack memory optimization
that didn't make Go 1.27 (https://go.dev/cl/807260)

Updates #29053

Change-Id: I5c694d8c4aebf4854773ea2856177aaebb2c39a4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-09-01 04:47:30 -07:00
Martin Minkus b25459ab1f tstest/natlab/vmtest: add multi-flow FreeBSD SNAT test
The existing TestSubnetRouterFreeBSD makes a single HTTP request, which
cannot catch a NAT rule whose translation address is wrong on average
but right occasionally: with a PF "-> (self)" rule, pf round-robins new
states across every address on the machine, so one request has decent
odds of drawing the working address while most flows hang at SYN. That
made the single-flow test an unreproducible coin flip rather than a
regression gate.

Open several fresh flows and require that all of them complete and that
the backend sees exactly one source address. PF rule counters and the
state table are dumped along the way, so a failure shows which address
each flow was translated to.

Fails 4/8 (alternating) against a "-> (self)" NAT rule, with the state
table showing the dead flows translated to the QEMU debug NIC's address
and to tailscale0's own address. Passes 8/8 with per-egress-interface
rules, and under netstack subnet routing.

Updates #5573

Change-Id: I1739ceefbab7764f1db8d07424832947b1d962a3
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus d80f3f7e6e cmd/tailscaled: keep handling FreeBSD subnets in netstack by default
Moving FreeBSD subnet routing from netstack to the kernel changes the
behavior of every existing FreeBSD subnet router, and the kernel path is
not yet ready to be the default:

  - The pf NAT rule we install never matches. On a production FreeBSD
    15.0 subnet router, "pfctl -vsn -a tailscale" reports 97071
    evaluations with 0 packets and 0 translations, and "pfctl -s info"
    reports translate: 0. So --snat-subnet-routes=true, the default,
    silently performs no source NAT at all.

  - Inserting the pf anchor at runtime requires reloading the main
    ruleset, which cannot preserve the contents of any table that
    ruleset references. See the comment in osrouter.loadPFMainRuleset.

Netstack does its own SNAT in userspace, touches no system state, and is
what FreeBSD has always used, so keep it as the default. The kernel path
can be opted into with TS_DEBUG_NETSTACK_SUBNETS=false; only that path
can serve --snat-subnet-routes=false, which netstack cannot do because
it must rewrite the source address.

With this, TestSubnetRouterFreeBSD passes.

Updates #5573

Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 4e80553a9d wgengine/router/osrouter: only remove PF anchor refs this process added
removePFAnchorRef ran at shutdown (and from the startup cleanup hook) and
unconditionally rewrote the main PF ruleset to strip the "tailscale"
anchor references. That is wrong twice over for an operator who
configured those references statically in /etc/pf.conf -- the durable
setup the ensurePFAnchorRef error message recommends:

  - it drifts the running ruleset away from /etc/pf.conf on every
    tailscaled shutdown, and
  - the rewrite reconstructs the ruleset from "pfctl -s" output, which
    names PF tables but never their contents, so any table the operator
    populates out-of-band is silently emptied (same failure mode
    ensurePFAnchorRef now refuses).

Track whether this process inserted the references and only remove what
we added, and even then leave them alone if the ruleset now references
tables. Skipped removal is harmless: callers flush the anchor's contents
first, and a reference to an empty anchor has no effect on traffic. The
flag is process-local by design, so the cross-process startup cleanup
hook never removes references it cannot prove are tailscaled's.

The only-remove-what-we-added semantics were first identified and
implemented by Ross Williams (@overhacked) on a fork of this branch;
this is an independent implementation of the same idea alongside the
table guard.

Updates #5573

Change-Id: Idf579a8b4190c37211782feac4e5bfd057c3ace0
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 4c5862376c wgengine/router/osrouter: don't clobber PF tables when adding anchor refs
ensurePFAnchorRef reconstructs the main ruleset from "pfctl -sn" and
"pfctl -sr" output and reloads it with "pfctl -f -". That output names any
table a rule references but never prints its contents, so the reconstructed
ruleset re-declares every table as empty. Reloading it silently drops the
addresses of any table the operator populates out-of-band -- a "persist file"
table, "pfctl -T add", pfctl's own automatic tables for interface groups --
and every rule referencing that table then matches nothing.

That is a quiet, security-relevant failure: a ruleset whose "pass ... from
<trusted>" rules still exist but match no addresses looks fine in "pfctl -sr".

There is no way to insert an anchor reference into a running ruleset without
a reload, so detect the case and refuse, pointing the operator at the durable
fix (putting the anchor references in /etc/pf.conf, where they survive reboots
and pf reloads anyway).

Boxes that already have the anchor references configured are unaffected:
ensurePFAnchorRef returns early before reaching this check.

Updates #5573

Change-Id: I6efa9a63e1322af2ec8fd986a774ed76c5d5766e
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 827c6fe50d wgengine/router/osrouter: SNAT to the egress interface address, not (self)
The FreeBSD subnet-router NAT rule translated with "-> (self)":

  nat on ! tailscale0 inet from 100.64.0.0/10 to any -> (self)

In pf, "(self)" is a round-robin pool of every address on the machine,
including tailscale0's own address and loopback, and pf deals each new
state the next address in the pool. Only flows that happen to draw the
egress interface's address work; a flow translated to any other address
gets replies the far end cannot route, and hangs at SYN. With N usable
addresses on the box, roughly (N-1)/N of connections through the subnet
router silently fail.

Observed in a natlab vmtest against a FreeBSD 15.0 subnet router with
four addresses (WAN, LAN, QEMU debug NIC, tailscale0): exactly half of
8 HTTP requests hung, alternating, and the pf state table showed the
failed flows translated to the debug NIC's address and to tailscale0's
own address:

  10.0.0.102:51100 (100.64.0.1:35132) -> 10.0.0.103:8080  ESTABLISHED
  10.0.2.15:56553  (100.64.0.1:35148) -> 10.0.0.103:8080  SYN_SENT:CLOSED
  100.64.0.2:52655 (100.64.0.1:54106) -> 10.0.0.103:8080  SYN_SENT:CLOSED

On a production FreeBSD firewall running this branch, the equivalent
IPv6 rule shows 55 state creations totalling 117 packets (about two
packets per state): SYNs whose replies never came back.

Emit one rule per up, non-loopback, non-Tailscale interface instead,
translating to that interface's own address, per address family only
where the interface holds a usable address of that family:

  nat on vtnet0 inet from 100.64.0.0/10 to any -> (vtnet0)
  nat on vtnet1 inet from 100.64.0.0/10 to any -> (vtnet1)

which is also the rule form FreeBSD firewall operators write by hand.
With this, the same 8-request test passes 8/8, and the LAN interface's
rule shows 8 states with healthy packet counts (56 packets total).

The interface set is sampled when SNAT is enabled; interfaces added
later are not covered until SNAT is toggled or tailscaled restarts.

Updates #5573

Change-Id: Ife3367124737ce5c8785ca7f920eafca593ec705
Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 20:31:01 -07:00
Brad Fitzpatrick 1293b4f67b wgengine/router/osrouter: add native FreeBSD routing, no-snat support
And add site-to-site natlab vmtests for Linux + FreeBSD.

Fixes #5573
Updates #18897

Change-Id: I9629f46fdd99137f0ae96431487d3a7410e2a4cc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-31 20:31:01 -07:00
Martin Minkus 58f28a1927 net/netutil: check actual IP forwarding sysctls on FreeBSD
CheckIPForwarding unconditionally returned a "not currently officially
supported" warning on FreeBSD without ever reading the forwarding
sysctls. Subnet routers on FreeBSD therefore got a spurious "IP
forwarding is disabled" health warning and admin console banner even
with net.inet.ip.forwarding=1 and net.inet6.ip6.forwarding=1 set.

Read the sysctls instead, and only warn for the protocols actually
required by the advertised routes. FreeBSD has no per-interface
forwarding knob, so only the global sysctls are checked.

dragonfly, netbsd and openbsd keep the previous unsupported warning.

Updates #5573

Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
2026-08-31 19:15:51 -07:00
Mike Jensen 650bb0546a fuzz: Don't pin go version, allowing go 1.27 support (#21057)
This change expands on #20862 to allow our oss-fuzz.sh script to be flexible for go1.27 (and future go versions without needing oss-fuzz changes).

The [PR on oss-fuzz](https://github.com/google/oss-fuzz/pull/15996) has been updated to no longer pin the go version.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: If5c7fe9e4171d9f31b4a767e9ec372d5e0edbe0d

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-08-31 17:49:18 -06:00
James Tucker 57c3357fdb cmd/tailscale: allow IPv6 localhost serve targets
Use net.JoinHostPort when expanding proxy targets so IPv6 loopback addresses retain the required brackets. Accept ::1 as an HTTP and TCP destination in both current and legacy serve implementations.

Fixes #8702

Signed-off-by: James Tucker <jftucker@gmail.com>
2026-08-31 14:25:58 -07:00
Mike Jensen fae8f241bf fuzz: wire fuzzing to a standalone oss-fuzz script (#20862)
This change adds an entry point for oss-fuzz `fuzz/oss-fuzz.sh`, allowing us to wire in our current and future fuzzing into oss-fuzz without needing to update the google/oss-fuzz repo.

Existing fuzzing was also reviewed with the following changes:
  * disco/disco_fuzzer.go renamed to disco/fuzz_test.go so that it can have a _test.go suffix and match the modern go fuzzing design.
  * net/stun/stun_fuzzer.go renamed to net/stun/fuzz_test.go similar to the above
  * Disco and stun recieved seeds for their fuzzing starts
  * All existing fuzzing was given a local round of testing, which resulted in a round trip fix for disco not handling a full zero node key.
  * Running and building fuzzing was removed from CI (build only). The fuzz seeds are validated in normal go testing, but the fuzzing itself will only happen if run manually or on oss-fuzz.

Updates https://github.com/tailscale/corp/issues/46608

Change-Id: I47cb70169aefb02ac5a56220f26a6ec07fa135ee

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-08-31 11:59:06 -06:00
Brad Fitzpatrick d9cc55e33b tsnet: avoid depending on mutable DefaultTransport
Revision to earlier 49e148c4a3 to be robust in case somebody
unwisely edits global variable net/http.DefaultTransport.

Updates #21034

Change-Id: I0d4eb63d97c84641ffe45ac43fa4f3ccffc7446d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-31 10:15:53 -07:00
Brad Fitzpatrick 2a4d74356b wgengine/netstack: fix data race on test logger in makeHangDialer
The dialer returned by makeHangDialer runs on netstack-owned goroutines
that can outlive the test, so calling tb.Logf from it raced with the
test completing. Use tstest.WhileTestRunningLogger, which stops logging
once the test is done, as makeNetstack already does.

Fixes #21052

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ibbf4288eff48f3ae0bb5812204a1cdfd567322a3
2026-08-31 09:19:31 -07:00
Brad Fitzpatrick 49e148c4a3 tsnet: base HTTPClient's transport on http.DefaultTransport
Previously Server.HTTPClient returned a client whose transport set
only DialContext, losing http.DefaultTransport's defaults such as
ForceAttemptHTTP2, MaxIdleConns, IdleConnTimeout, TLSHandshakeTimeout,
and ExpectContinueTimeout. Now it clones http.DefaultTransport and
overrides DialContext to dial over the tailnet, so it picks up those
settings and any future defaults.

Proxy is explicitly nil: an HTTP proxy from the environment would be
dialed through the tailnet, where it's unlikely to be reachable.

The new test compares the transport field by field against a clone of
http.DefaultTransport and fails on any unknown future field, forcing a
decision about how HTTPClient should handle it.

Updates tailscale/corp#47393 (as motivation)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I1e800982ee11d386699dd4f588c1fd2ce392f229
2026-08-30 15:34:44 -07:00
Brad Fitzpatrick 72780705ed net/tsdial: don't touch the peerapi transport in Close when omitted
Dialer.Close unconditionally called PeerAPITransport, which panics
when the binary is built with the ts_omit_peerapiclient build tag,
so any such binary crashed on shutdown. Skip the idle connection
cleanup in that case; there is no peerapi transport to clean up.

Updates #12614

Change-Id: I6a8fd1860f8b74407fbb12c9a46d5fb07711e142
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-30 07:45:38 -07:00
Winter M 33af330c8e flake.nix: update for go 1.27
Signed-off-by: Winter M <winter@antithesis.com>
2026-08-28 12:47:04 -07:00
basavaraj-sm05 60576f8bd1 net/socks5: compare proxy auth credentials in constant time
The SOCKS5 server checked the client-supplied username and password against the configured credentials with plain string equality, which returns on the first differing byte. In tsnet the password is a random 128-bit value that gates every dial out through the node, and the listener is on 127.0.0.1, so a local process can time the auth reject to recover it a byte at a time with unlimited attempts and no lockout. The LocalAPI sharing the same loopback listener already compares its credential with subtle.ConstantTimeCompare; do the same here for both fields, evaluating both so the username result does not gate whether the password is examined.

Updates #20998

Signed-off-by: basavaraj-sm05 <basavaraj@digiscrypt.com>
2026-08-28 11:53:43 -07:00
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
Brad Fitzpatrick faa10ddaaa go.toolchain.branch: switch to Go 1.27
Promote the toolchain from Go 1.26.6 to Go 1.27.0, matching what
go.toolchain.next.rev has been testing. Besides the toolchain files
themselves (updated by pull-toolchain.sh), this bumps the go.mod go
directive, the Dockerfile golang base image, and the README, and
regenerates the depaware.txt files and the gzip assets in
tempfork/spf13/cobra and util/eventbus, whose bytes change with
Go 1.27's rewritten compress/flate.

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

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

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

Updates #20220

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

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

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

Fixes tailscale/corp#46577

Signed-off-by: Becky Pauley <becky@tailscale.com>
2026-08-27 07:46:59 +01:00
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
Prakash RudrarajuandClaude Opus 5 ec2ca5964b tsnet: don't panic in Close when Start failed before s.sys was set
Server.close() dereferenced s.sys.Bus unconditionally. s.sys is only
assigned partway through doInit, so it is still nil when Start fails
before that point, and Close then panics with a nil pointer dereference.

Close is reachable in that state: Start is idempotent via initOnce and
returns the stored initErr, so callers that `defer Close()` before
checking the error land in close() with s.sys == nil. Every other
resource released nearby (s.dialer, s.localAPIListener,
s.loopbackListener) is already nil-guarded; this line was the outlier.

This is reachable in practice from libtailscale/TailscaleKit, where a
failed tsnet start on darwin is followed by a close and surfaces as
EXC_BAD_ACCESS rather than the underlying start error.

Use GetOK instead of Get as well, so an unset Bus returns ok=false
rather than panicking via SubSystem.Get.

Adds TestCloseBeforeStart, which panics without this change.

Updates #20988

Signed-off-by: Prakash Rudraraju <1471544+prakashrj@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 13:47:29 -07:00
Brad Fitzpatrick d7253cb40e tsnet: add test and docs for using an exit node
tsnet nodes have long been able to use an exit node by setting the
ExitNodeID pref through LocalClient, but that was neither documented
nor tested.

Add a package doc section (and regenerate the README) showing how to
select an exit node via EditPrefs, and add TestDialThroughExitNode
verifying that a Dial of a non-tailnet IP is routed over WireGuard to
the exit node rather than dialed from the host network.

Fixes #20986

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia1715a628245c52163083803889fd1f475abfab7
2026-08-26 06:31:01 -07:00
Brad Fitzpatrick af4b7b0363 tsweb/varz: add goamd64_capable and goamd64_compiled default metrics
To decide whether we can bump GOAMD64 for our production binaries, we
first need to know what x86-64 microarchitecture levels the fleet's
CPUs actually support. Add two gauges to the default metric set on
linux/amd64: goamd64_capable, the maximum GOAMD64 level the host CPU
supports per /proc/cpuinfo, and goamd64_compiled, the level the
running binary was built with per its build info. Both are computed
once at init and report 0 on error.

The per-level feature lists match the checks in Go's
runtime/asm_amd64.s.

Updates tailscale/corp#47099

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I3b4e5431ae1501f0b7d584c12d9af805e9ab6125
2026-08-25 07:23:04 -07:00
Claus Lensbøl da1fc4fc8c wgengine/magicsock: add facilities to support multiple disco keys (#20927)
Instead of accessing the key and string directly, put it behind a method
to make it easier to use that as a proxy when we add multiple keys
(control and TSMP origin).

Instead of only updating a single origin for disco keys, teach peermap
how to work with multiple disco key sources so a key can originate from
either control or TSMP.

This sets the work up for switching dynamically between disco keys
later. As of this commit, the keys still arrive for the most part via
the controlClient, but this commit sets us up for:
 - Switching dynamically between received keys.
 - Route TSMP keys directly into magicsock, not via controlClient.
 - Potentially revert controlClient to the state before any TSMP
   changes, making it single threaded.
 - Use switching keys as trigger for optimistic WG handshakes, instead
   of tearing down the full connection and setting it up again.

Updates #20494

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-08-25 09:55:48 -04:00
Alex Valiushko 65d2226742 wgengine/magicsock: reset endpoint trust on rekey (#20368)
Invalidate the trust window so we re-run path discovery
instead of coasting on the dead path until trust lapses on its own.

Partially reverts 85bb5f8 removal of resetLocked within updateFromNode.

Fixes #20268

Change-Id: Ib18b31e4329513294b682e698d1a9f426a6a6964
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-08-24 14:25:25 -07:00
Brad Fitzpatrick 75519889f5 derp, cmd/derper: relay client app names to watchers, allow banning them
Clients can advertise an opaque app name in their ClientInfo but the
server previously did nothing with it.

Constrain app names to at most 32 bytes of printable ASCII, enforced
both in derp.NewClient and by the server when it parses the ClientInfo.

Extend the peerPresent frame, following its existing pattern of
appending optional fields, with a length-prefixed app name after the
flags byte, so trusted mesh watchers (other DERP nodes and stats
tools) can attribute connections by app. Old clients ignore the extra
bytes; old servers send frames without them.

Also add a derper --disallow-app-names flag taking a comma-separated
list of app names whose connections are refused, except for trusted
mesh peers.

Updates tailscale/corp#24454

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I6e721258675145833aafa1355fabf7fc05a5a204
2026-08-24 08:37:00 -07:00
Brad Fitzpatrick 6872f1c4b0 derp: add test of client peerPresent parsing across server versions
Test that the client parses peerPresent frames from servers of various
eras: old servers that send fewer fields than the client knows about,
and newer servers that send trailing fields the client doesn't know
about, which it must ignore. This matters during rollouts of new DERP
servers, when a region's meshed nodes and watchers run a mix of
versions.

This is in advance of a following commit that extends the frame with a
new trailing field.

Updates tailscale/corp#24454

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I0d46126a6003a9eb07a4da46dc291872149b1a07
2026-08-24 08:37:00 -07:00
Brad Fitzpatrick c13a1f9c55 tstest: coalesce dropped Ticker ticks when advancing the clock far ahead
Clock.Advance previously fired each Ticker once per period between the
current time and the advance target, even though all but the first few
sends were dropped on the Ticker's full channel. A test that advances
the clock 180 days with a 5 second ticker registered did 3.1 million
fires, each taking a mutex, attempting a channel send, and fixing the
event heap. Under the race detector, where synchronization operations
are 20-30x more expensive, this dominated test runtime.

Now, when a tick is dropped because the channel is full, skip the
remaining missed ticks in one step, staying aligned to the original
schedule and still firing the final tick at or before the advance
target. This matches time.Ticker, which drops ticks it cannot deliver,
and is observably identical behavior. Ticks that fit in the channel are
still all delivered.

In the internal corp repo, this takes controlclient.TestExpiry under
race from 6.0s to 2.2s (its no-race time is 0.5s), and the new
benchmark improves from 449µs to 90ns per 24h advance:

  BenchmarkTickerLargeAdvance-16  100       449313 ns/op    (before)
  BenchmarkTickerLargeAdvance-16  13311910      90.22 ns/op (after)

Updates tailscale/corp#47035

Change-Id: I4826df212415eb2ea7ceb5bc46848e816f8ccde1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-24 08:32:12 -07:00
Brad Fitzpatrick 5428027685 go.mod: bump github.com/go4org/hashtriemap for slight speed boost
(bumping in oss mostly to get it into corp, and we require them to be in sync
for now. But we do use this in derpserver too.)

Updates tailscale/corp#46884

Change-Id: If01e3e91787e9ec4331eb9c997292239c6b379f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-23 22:37:00 -07:00
Paul Scott ed7d521cae go.mod: upgrade go4.org/netipx (#20967)
Bring in perf improvements from go4org/netipx#22

Diff: https://github.com/go4org/netipx/compare/fdeea329fbba...3075585bcbeb

Updates tailscale/corp#46884

Signed-off-by: Paul Scott <408401+icio@users.noreply.github.com>
2026-08-23 18:26:21 +01:00
Brad Fitzpatrick de9ec7ee25 go.mod: bump x/net for http2-in-std fix for Go 1.27
There's a deadlock in x/net/http2 with Go 1.27 that's
fixed with v0.58. This is prep for switching to Go 1.27.

Updates #20220

Change-Id: I8e81eafc0c2f78e1760b36e9d39ac4f6046565af
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-22 11:48:56 -04:00
Bonobo 86e5d3873a cmd/tailscale/cli: stop Serve port ranges at 65535
The set-config loop uses uint16 endpoints. When Last is 65535, the increment wraps to zero and file or Unix targets continue indefinitely.

Break after applying Last so every closed range terminates without changing ordinary range behavior.

Fixes #20873

Signed-off-by: Bonobo <github@in9.at>
2026-08-21 14:23:06 -06:00
Adriano Sela Aviles 6d7f1212b7 go.mod,go.sum: bump wireguard-go to fix AllowedIPs mem leak
Pulls in https://github.com/tailscale/wireguard-go/pull/85 which
fixes an unbounded memory leak in mkIPInCIDRsTestFunc. It used a
package-level placeholder Peer, and AllowedIPs.Insert threads every
trie node onto that peer's trieEntries list, so each call leaked
its whole trie. SetAllowedIPs calls it on every netmap update, so
peer churn accumulated trie nodes until the client OOMed.

Fixes tailscale/corp#47010

Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
2026-08-21 12:44:21 -07:00
Brad Fitzpatrick 886d1b2e63 control/controlclient: return ctx.Err() from canceled map polls
When a map poll's context is canceled mid-read, the error that
surfaces from the response body read depends on the transport and Go
version: Go 1.26's http2 returned the context error, while Go 1.27's
returns the underlying "use of closed network connection". Normalize
the read-error paths in sendMapRequest to report ctx.Err() when the
context was canceled so callers see a stable, meaningful error
regardless of Go version.

Updates #20220

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I5898d3f452a2e61cfdf3ee015386365943c08e70
2026-08-21 14:36:59 -04:00