Windows socket setup enumerates adapters and the routing table through
GetWindowsDefault for every netns socket. These calls can be expensive,
particularly on systems where security software adds syscall latency.
Cache nonzero default interface indices per monitor and address family,
and use them from netns controlC. Invalidate on native route, address,
and IP interface change notifications.
Coalesce concurrent lookups and retry results invalidated during lookup.
Bound lookup and wait attempts; once the budget is exhausted, fall back
to a single uncached lookup rather than failing socket setup, erring
toward a possibly stale interface index over no socket at all. Perform
fresh lookups for zero indices and errors, and retain the existing
unbound-socket fallback for a fresh zero result.
Use uncached lookups after monitor shutdown or when interface-change
notification registration fails.
Test cache reuse, independent address families, notification invalidation,
concurrent fills, invalidation during lookup, route restoration after a
zero result, churn and wait-budget fallbacks, and shutdown.
Fixes#21885
Signed-off-by: James Tucker <james@tailscale.com>
In #20775 (and the follow-up in #21471), we started installing quad-100
as the OS's primary resolver whenever cfg has hosts not covered by a split
DNS route, so that sandboxed macOS clients could still answer them.
However, that check was added to the generic split DNS gate rather than
to the Apple-specific one, so it also applies on Windows, where dcfg.Hosts
always carries every node's records and MagicDNSHostsUnrouted is set
whenever MagicDNS is disabled. As a result, a Windows node with MagicDNS
off and at least one split DNS route (or with MagicDNS on and a peer shared
in from another tailnet) gets a catch-all NRPT rule pointing at quad-100,
and all DNS traffic on the device is proxied through it instead of only
the configured suffixes.
The check is redundant for Apple platforms: iOS never takes the scoped
path there, and sandboxed macOS only does so via scopeApple, which already
requires !cfg.requiresPrimaryResolver(). This removes it from the generic
gate, restoring the pre-1.104 behavior on Windows, and adds tests.
Updates tailscale/corp#45534Fixes#21863
Signed-off-by: Nick Khyl <nickk@tailscale.com>
Create a fresh directory for each Linux tarball download and remove it
after unpacking or a failed download. Continue to prefer the user cache
directory and use the temporary directory when the cache path is
unavailable.
Fixestailscale/corp#50119Fixestailscale/corp#48680
Change-Id: Iebe13d922f8fa691f26fe17d908b295b7153aadb
Credit: Winfunc
Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
Package unitconv implements string conversion functionality
for unit prefixes such as those from the SI and IEC standards.
It is catered towards printing of values for human consumption.
This exists to support open-sourcing JSON migration tooling.
Updates #20220
Updates tailscale/corp#791
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
launchProcess duplicated the pty master with syscall.Dup, which is a
plain dup(2) and does not set FD_CLOEXEC. The original master from
pty.Open is CLOEXEC (os.OpenFile), but the dup is not, and it stays
open in tailscaled until the session's stdout copier closes it.
Because child processes are started with os/exec, which relies on
FD_CLOEXEC to drop fds, every other SSH session started while this one
is alive inherits the dup'd master. It then survives the incubator's
exec into login, the user's shell, and everything the shell runs.
When the original session disconnects, tailscaled closes its own copies
but the leaked copies in other sessions' process trees keep the pty
open: the slave never hangs up, nobody reads the master, and any process
writing to that tty blocks forever once the buffer fills. On a host with
a long-lived tmux server this shows up as every tmux command hanging,
with the server stuck in n_tty_write on a dead client's tty.
Use F_DUPFD_CLOEXEC so the dup is never inherited across exec.
Fixes#21343
Signed-off-by: Sachin Nene <sachin@corvex.ai>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump aws-sdk-go and related dependencies so that we can drop our
aws-vault fork in tailscale/corp.
Updates #cleanup
Change-Id: I9b73a8d637eb9e9e57c508797ad0fe91c4391e0c
Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
When receiving a PCP request, a NAT-PMP-only server must reply with
an unsupported version response. Same for a PCP-only server
receiving a NAT-PMP request.
The tailscale client sends both a NAT-PMP and a PCP request when
probing for portmapping support and it correctly handle any
unsupported version response it receives.
Updates #21713
Change-Id: I0ec8b4a6c1de14bcc64b82f1988cde7fc01f5747
Signed-off-by: Francois Marier <francois@tailscale.com>
The immediate use case is feature/conn25 identifying the connector peer
node that responds to a DoH request.
Updates tailscale/corp#46169
Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
Adds tests that only a 404 map response wraps ErrNodeRemoved, that the
node-removed warning and NeedsLogin state are cleared by a new netmap or
a profile switch, and that a node restarted after it was deleted shows
the warning and reaches NeedsLogin.
Updates #18700
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
When a node is deleted, control answers its map requests with a 404 and
the client keeps retrying in its current state, so watchers like
containerboot can't tell a deleted node from a lost connection. This
wraps that error in ErrNodeRemoved, and LocalBackend then moves to
NeedsLogin and sets a node-removed health warning, both cleared on the
next netmap or profile switch. It repurposes #18701 as a stopgap until a
broader fix, such as structured errors from control, exists.
Updates #18700
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
runServeSetConfig built every non-file, non-unix endpoint target as
"<protocol>://<host:port>", so a tls-terminated-tcp endpoint produced the
target tls-terminated-tcp://host:port. applyTCPServe accepts only the tcp
and unix target schemes, and it takes TLS termination from the serve type
rather than from the target. Every such endpoint therefore failed with
"unable to expand target". The flag form already works because it passes
--tls-terminated-tcp=<port> together with a tcp://host:port target.
Build the target with the tcp scheme whenever the serve type is TCP or
TLS-terminated TCP. Plain tcp endpoints already used that scheme, so only
tls-terminated-tcp endpoints change. https+insecure needs no mapping, since
the web path accepts it as a target scheme, and unix targets skip scheme
construction entirely.
Fixes#21786
Signed-off-by: Vivek1898 <72497717+Vivek1898@users.noreply.github.com>
We're going to add a second SO_REUSEPORT use in the codebase. Extract
and generalize udprelay mechanism for future reuse. Also address the
Linux flaw of allowing same UID groups to merge if the ephemeral port
selection collides.
Exposes public SetReusePort, RawConn and ControlFunc signatures for
shared use.
Updates tailscale/corp#48168Fixestailscale/corp#49297
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
Change-Id: Iba33142877513d72123c51cf1bcc33cf6a6a6964
On a running node with AdvertiseTags set, a bare "tailscale up" never
returned. updatePrefs compared the stored tags with the flag-derived
prefs, which have no tags since no flags were given, concluded that
the tags had changed, and skipped the justEdit path. runUp then sent
the simple up's WantRunning=true edit, a no-op on a running node, and
waited on the IPN bus for a running state that was never sent.
A simple up keeps the current settings, so don't treat its missing
tags as a change.
Fixes#21774
RELNOTE: Fix "tailscale up" with no flags hanging on a running node with advertised tags.
Signed-off-by: Evan Lowry <evan@tailscale.com>
Co-authored-by: Zach Buchheit <zbchht@gmail.com>
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>
A naive sync.Pool that stores dynamically sized []byte
is incorrect since the Pool assumes that each stored item
carries approximately the same memory cost.
Different cost items should be stored in different pools.
The bufpool tracks buffer pools by powers of two
and provides a Buffer type that mimics the API of bytes.Buffer.
This is a performance optimization. Avoid using this unless
there is a clear performance gain for doing so.
Incorrect use can lead to widespread data corruption
(as is already the case with any incorrect sync.Pool use).
Updates tailscale/corp#21363
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This makes the syspolicy cli use the caller's user scope instead of the
generic user scope. In tailscale/tailscale#21701, we made the LocalAPI
policy endpoint resolve unscoped policy requests to the connected user's
policy. However, the cli serialized setting.DefaultScope() as
/policy/User, which selected the general user policy and not the User(SID) policy used by
IPN watchers.
This was tested by changing the AlwaysOn policy and verifying that the
User(SID) policy was reloaded and the gui's IPN policy change callbacks
fired.
Updates tailscale/corp#41463
Signed-off-by: kari <kari@tailscale.com>
The relay server force-sets its UDP socket buffer sizes. The kernel
requires the NET_ADMIN capability for this. Without it, tailscaled logs
a warning and uses the kernel defaults. Dogfooding showed lower
throughput and more dropped packets in that state.
This commit adds NET_ADMIN to the tailscaled container in peer relay
StatefulSets. A ProxyClass that sets a security context on the
Tailscale container replaces the default, as for other proxy types.
Fixes#21745
Change-Id: Ic84e243d7e6184afa4d0bb0c38c4be8eb899261b
Signed-off-by: David Bond <davidsbond93@gmail.com>
Bump the oras.land/oras-go/v2 indirect dependency from v2.6.1 to v2.6.2,
which contains the fixes for both govulncheck symbol findings on main
Updates #cleanup
Change-Id: Ibb4aa08823d900a6b8f7f9440a64fb4ce4bfe659
Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
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>
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>
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>
Repeated CallMeMaybe messages over DERP can each log a new contact for
the same peer, flooding stderr. Mark the DERP, direct, and control-netmap
latency messages [v1] so they remain in diagnostic uploads but only
reach stderr with --verbose=1.
Updates #12639
Updates #21765
Change-Id: Ic7ee47c699e5698f61358a72c6fd7776e1caf8dc
Signed-off-by: kenneth topp <toppk@bllue.org>
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>
Updates tailscale/corp#49775
Track authoritative NXDOMAINs for at least 60 seconds on macOS and iOS (where our short SOA ttls are not properly respected), and flush when a cached name becomes resolvable. Compile a no-op tracker elsewhere and avoid duplicate VPN reconfiguration on link changes.
On a fresh install the first netmap arrives before the first netcheck
report, so auto exit node resolution installed the blackhole
ipn.UnresolvedExitNodeID and nothing re-resolved it once the report
arrived. Refresh in setNetInfo while the exit node is still unresolved.
Fixes#18199
Signed-off-by: Brendan Creane <bcreane@gmail.com>
This implements enough of PCP to establish direct connections
with a node behind a hard NAT.
Updates #21713
Change-Id: I44b955e9b8ecce80ddc49afdfb01eb49c8d56f8c
Signed-off-by: Francois Marier <francois@tailscale.com>
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>
The reconciler forced the AWS load balancer address type to ipv4, so on
an IPv6 EKS cluster, where pod targets are IPv6, no Network Load
Balancer was ever provisioned.
Make the address type a default that spec.service.annotations can
override, defaulting IPv6 source NAT on when dualstack is requested.
Resolve load balancer hostnames for both address families and requeue
ready PeerRelays every five minutes, since DNS records for a new load
balancer arrive over several minutes. Add spec.service.ipFamilyPolicy
and ipFamilies so dual-stack GKE and AKS clusters can request a
dual-stack load balancer too.
Shorten the PeerRelay field descriptions and link to the PeerRelay docs
for the detail.
Fixes#21399
Change-Id: Idf22eb02f8ed9a46ff4abea8d4faef4b8c12c111
Signed-off-by: David Bond <davidsbond93@gmail.com>
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>
tailscale debug ts2021 now starts with an advisory TLS check against
the control plane over port 443: it compares a TLS 1.3 handshake using
Go's default key exchange groups (which include ML-KEM hybrid
post-quantum groups and produce a large ClientHello) with one
restricted to classic groups (a small ClientHello). If the large
ClientHello fails while the small one succeeds, the network path does
not support large (post-quantum) ClientHellos, likely due to a
middlebox dropping ClientHellos that span multiple TCP segments.
The check honors --connect-ip (dialing that IP while keeping the host
for SNI), can be skipped with --skip-tls-check, and never aborts the
surrounding command.
Updates #21708
Signed-off-by: James Tucker <james@tailscale.com>
Since 7e609b258 (v1.102), quad-100 looks up MagicDNS names in
nodeBackend.nodeByName, which also holds short-name keys for in-process
dials. So a query for "foo." matched peer "foo.tail-scale.ts.net" and
was answered with its Tailscale IP. 1ec348784 fixed that for MagicDNS
off; this fixes it for MagicDNS on, too, matching pre-1.102 behavior.
The OS applies the search domain to short names before they reach
quad-100, so a single-label query is a root zone name, not a tailnet
name, and must be forwarded (e.g. to LAN mDNS/LLMNR or a global
nameserver). Short names still work for in-process dials via
NodeByName.
Updates #20789
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ic8199a96cf79178210c9a85e0cebfb46567a894a
This will make the addition of PCP support easier.
No changes to NAT-PMP support or tests.
Updates #21713
Change-Id: I7ac58badbd4e8680c0e50d8dd5d78d6d0abb4aa0
Signed-off-by: Francois Marier <francois@tailscale.com>
To faciliate roaming a few things had to happen as tta communicated over
the same links as tailscaled communicates over, meaning that the
connection to tta gets cut when cutting the interface.
- Whenever we have connected to the client after waiting for it to come
up, clean up idle connections.
- Have tta use keepalives to reconnect.
- Delete default route on just via, not the interface.
- Add capabilities to QMP for setting the interface state.
- Implement the test.
Fixestailscale/corp#49639
Signed-off-by: Claus Lensbøl <claus@tailscale.com>
Health tracker no longer holds a pre-set number of receive func health
items. Magicsock can spawn an arbitrary number of new health items
ad-hoc instead.
A tracker that no magicsock registered with no longer reports a missing
receive func, including builds without UDP transport.
Updates tailscale/corp#48168
Change-Id: I1f929db5a9f056d41a590cebd87a5cfdecf55510
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
* k8s-operator,sessionrecording: fix SPDY protocol DoS risks
There was a risk that a malicious kubectl client could cause a DoS issue for the Tailscale proxy performing a k8s recording by defining large quantities of headers, or using a decompression bomb.
This change adds limits of 1000 header pairs, and a maximum of 1MB for the decompressed header block size. In addition fuzz testing is added and integrated with oss-fuzz to review for other protocol issues.
Credit to Eresus Security for reporting the header quantity DoS risk.
Fixestailscale/corp#45112
Updates tailscale/corp#46608
Change-Id: Ie27b241b9f1cac58198560a88a44ffaa0927b96e
Signed-off-by: Mike Jensen <mikej@tailscale.com>
* k8s-operator/sessionrecording/spdy: test consecutive header blocks share one zlib context
kubectl compresses every stream's header block against one zlib stream for the
life of the connection, so the proxy decodes them all through a single
zlibReader (conn.zlibReqReader). This test compresses two blocks onto one writer
and decodes both from one reader.
It fails on the second block: parseHeaders' ended() reads one byte past the
first block, which on the shared context is the start of the next block, leaving
the reader unable to decode it ("unexpected EOF"). In a real session that means
every stream after the first fails to parse and its output is never recorded.
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
* k8s-operator,sessionrecording: parse full 31-bit SPDY stream IDs
dataFrameStreamID masked stream IDs to 7 bits while storeStreamID kept all 32, so output on any stream with ID >= 128 was never matched against a stored stdout/stderr stream and went unrecorded. Keep the 31 spec bits in both places. controlFrameVersion gets the matching 15-bit mask for its version field.
Mask storeStreamID to 31 bits as well: the reserved flag bit in a SYN_STREAM ID field can never appear in a data frame, so an ID stored with the bit set matches nothing.
Change-Id: Icf05a29a2838f2f2492b8d2da9e821f4e2459877
Signed-off-by: Mike Jensen <mikej@tailscale.com>
---------
Signed-off-by: Mike Jensen <mikej@tailscale.com>
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
Co-authored-by: chaosinthecrd <tom@tmlabs.co.uk>
Git for Windows 2.56.0 rejects GIT_CONFIG_GLOBAL=NUL with "unable to
access 'NUL': Invalid argument" (git-for-windows/git#6449), breaking
TestInfoFromEnvMatchesGit and TestInfoFromEnvPrecedence on Windows
runners with the new git. Point them at an empty file instead.
Updates git-for-windows/git#6449
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: If2b9b6d29344b9280771e507d7d7e6c06f7ab9b0
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>
ImportAliasCheck runs git grep in a subprocess, whose file reads Go's
test cache can't see. With a shared CI test cache, callers' tests kept
reporting a stale "ok (cached)" for new commits that added bad
x/exp/{maps,slices} imports, as long as the test binary didn't change.
That's how tailscale/corp#49716 merged with green CI.
Read GITHUB_SHA so Go records it in the test cache key, making the
cached result per-commit in CI.
Updates tailscale/corp#49716
Change-Id: I6301f5ed067a05294185b05a195550efa8e5aa01
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
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>