The test control server always listened on 127.0.0.1:9911, which is
useless for a node in a VM on the same machine. --addr picks the listen
address; the DERP and STUN servers follow it.
--ssh-policy loads a tailcfg.SSHPolicy from a JSON or HuJSON file and
sends it to every node, which also grants them the SSH node capability so
that "tailscale up --ssh" is accepted. That is what testcontrol.Server
already supported for tests; this exposes it for manual testing of the
Tailscale SSH server.
Updates tailscale/corp#47865
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I2b7c4e9f1a3d5c6e8b0f2a4d6c8e1b3f5a7d9c0e
The main server file mixed portable session handling with Unix details:
sending SIGHUP to end a session, decoding exec.ExitError, the euid check
for whether the process can switch users, agent forwarding's chown of the
socket, and reading /etc/ssh host keys as root. Those now sit behind small
functions (hangupProcess, waitProcess, canSwitchToLocalUser,
handleSSHAgentForwarding, systemHostKeyFile, isRootUser) in the new
process_unix.go, along with the incubator's forwarded-environment pipe
helpers, and the session's *exec.Cmd moves into an embedded osSessionState
struct defined there, so that the portable code no longer refers to the
process representation at all.
user.go keeps only the portable userMeta and userLookup; the login shell
and default PATH logic moves to user_unix.go. The SFTP child entrypoint and
its stdio adapter, which incubator.go and incubator_plan9.go each had a
copy of, move to sftp.go. The c2n usernames handler gains a hook for
platforms that list users some other way than /etc/passwd.
The agent socket's uid and gid are parsed as 31-bit rather than 32-bit
unsigned values so that the conversion to int for os.Chown cannot
overflow on 32-bit platforms, which is the pattern CodeQL flags.
Updates #cleanup
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I4c7e2b9a0d3f5e1c8b6a4d2f0e9c7b5a3d1f8e6c
dnsQueryForName builds the query for GET /dns-query?q=<name>, the peerAPI's
interactive debug mode. The name comes from the peer's query string and the
only thing done to it is appending a trailing dot, but the query is built
with dnsmessage.MustNewName, which panics as soon as the name is longer than
255 bytes.
The panic happens before the query reaches the resolver, so the nameAllowed
filter never gets a say and only sourceAllowed has to be true: any of the
user's own untagged devices, and any peer an extension hook lets through,
such as a client using this node as an exit node with DNS proxying allowed.
http.Server recovers it, so tailscaled survives, but the peer's connection is
dropped instead of answered and the node logs a panic trace per request.
Just under the limit the name was mishandled too: a 255-byte name is accepted
by NewName but rejected by Question when it packs the name, and that error
was dropped, so Finish returned a well-formed query with no question in it
which was then handed to the resolver.
Have dnsQueryForName use NewName, check the error from Question, and return
the error from Finish. handleDNSQuery turns a name it cannot build a query
for into the 400 it already uses for the other malformed-request cases.
Fixes#21307
Change-Id: I7c1a4b7e4f9a1d2c3b5e8f0a6d4c2b9e1f3a7d50
Signed-off-by: leoca <leo.camus23@gmail.com>
Update gVisor to include its fix for RACK loss detection with coarse
monotonic clocks. Configure netstack with the 500 microsecond clock
resolution used on Windows so RACK accounts for timestamp quantization.
Remove the TCP recovery override that disabled RACK, enabling gVisor's
default RACK behavior on all platforms.
Switch netstack to cubic congestion control. The int overflow in CUBIC
sender cwnd arithmetic that required pinning reno has since been reworked
upstream into float arithmetic with RFC 9438 target clamping.
Align the natlab vnet stack with netstack: enable cubic, and drop the
now-redundant explicit SACK and receive-buffer moderation sets, both of
which are gVisor defaults.
Fixes#9707
Signed-off-by: James Tucker <jftucker@gmail.com>
Payload guards a truncated packet by comparing both length and dataofs
against len(b), but the slice it returns is b[dataofs:length], so what
actually has to hold is dataofs <= length. Those are independent:
length comes from the IPv4 total length header field, while dataofs is
derived from the sub-protocol header, and decode4 never checks that the
declared total length covers the transport header.
A 28-byte IPv4/UDP packet declaring a total length of 20 decodes to
length=20, dataofs=28, len(b)=28, and Payload then evaluates b[28:20].
ICMPv4 and TCP reach the same state with 24- and 40-byte packets.
I found this by fuzzing Decode and then calling the accessors on the
result. I did not find a caller that can be driven into it from the
network: wireguard-go truncates decrypted packets to the declared IP
length, so on the inbound path dataofs > length implies dataofs >
len(b) and the existing guard already catches it.
Add a FuzzParsedPayload target that decodes and then calls Payload,
seeded with valid IPv4/IPv6 packets and with invalid ones, including
the three short total length packets above, and build it in
fuzz/oss-fuzz.sh.
Fixes#21231
Change-Id: Ie5d2100464b79750626b1bfefbe4020c4a42ca91
Signed-off-by: leoca <leo.camus23@gmail.com>
The nonce used in PCP identifies a particular port mapping. It needs
to be generated randomly for the initial mapping, and then saved so
that it can be used to renew or delete a mapping later. This is how
the router checks that we're authorized to change existing mappings.
If we receive a response for a different nonce, we should ignore it
since it's not meant for us (or the router is misbehaving).
Fixes#21127
Change-Id: Ic7874d376a74791807953f2f559c8a14a8eb75e9
Signed-off-by: Francois Marier <francois@tailscale.com>
The tsweb DebugHandler already links to expvar, Prometheus varz, and
pprof, but the only runtime visibility beyond pprof was the handful of
runtime.MemStats fields that varz special-cases. The runtime/metrics
package has far more (GC CPU classes, scheduler latencies, stop-the-world
pause histograms, heap breakdowns, and so on) and is the runtime's
preferred, cheaper interface.
Add a /debug/runtime-metrics handler. By default it serves only an index
of metric names, kinds, and descriptions, which are static, so viewing
the page does not read any values. The index is an HTML table for
browsers (Accept: text/html) and plain text otherwise; format=html or
format=text overrides the sniffing.
Values are read only on request, always as JSON:
- /debug/runtime-metrics/gc/heap/allocs:bytes returns that metric's
bare value, handy for scripts and curl.
- /debug/runtime-metrics?name=NAME (repeatable) returns a JSON object
keyed by metric name. A trailing * matches by prefix, so name=/gc/*
returns the GC metrics and name=* returns everything.
Histograms are objects with counts and buckets arrays; infinite bucket
boundaries, which JSON cannot represent, are the strings "-Inf" and
"+Inf".
In the HTML index, exact metric names link to the bare value form, and
each run of metrics sharing a directory is headed by a row linking every
ancestor directory to its wildcard query (/gc/*, /gc/heap/*, ...).
Responses inherit the debug handler's nosniff, framing, and CSP headers,
and application/json is not a script MIME type, so the values cannot be
pulled in cross-origin as a script by a malicious page.
Like the pprof handlers, this is excluded from js/wasm builds to keep
them small.
Updates #21300
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I7c2e9f4a1b8d3e6f0a5c9b2d4e7f1a3c6b8d0e2f
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>
Each connected client held two buffered channels of 32 pkts each (its
regular and disco send queues), about 5.5 KB per client for the
lifetime of the connection, even though almost all clients are idle at
any given moment.
Replace the channels with pktQueue, a mutex-guarded ring buffer whose
backing array comes from a per-server sync.Pool on first enqueue and
goes back when the queue drains empty, so an idle client holds no
queue memory at all. Enqueuers wake the client's sendLoop through a
one-slot channel.
Updates #21064
Change-Id: I2d7e8f4a1b6c3950e2a7d4b8f1c5e3a9d6b0c2e4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
The forwarder's upstream UDP socket is unconnected, so any host that
could send a datagram to its ephemeral port reached the single
ReadFromUDPAddrPort call that decided the query: a datagram with the
right 16-bit transaction ID was returned to the client as the
resolver's answer no matter what address it came from, and a datagram
with the wrong ID failed the query outright, handing it to the TCP
fallback (or to nothing at all, with TCP retries disabled).
sendUDP now keeps reading until a datagram arrives that could be a
reply to the query it sent: one from the resolver's address and port
carrying the transaction ID of the request. Other datagrams are
dropped and counted, so neither a spoofed reply nor a single stray
datagram can answer or end the query. This is the same source check
the kernel applies to a connected socket; the socket stays unconnected
because its ListenPacket path is what binds the query to a link
(IP_BOUND_IF on macOS, ForwardLinkSelector elsewhere). The read loop
still ends when the query context expires and closeOnCtxDone closes
the conn, as before.
On Windows an oversized datagram is reported as a truncation error
without a source address, so there the transaction ID remains the only
check, as before.
Thanks to Ben Carman for the report!
Updates tailscale/corp#48187
Reported-by: Ben Carman
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ibabbec5c82739b66caa6fb943faa376c39adbf64
TestOpenresolvDNS pins the case where Tailscale owns the only resolvconf
snippet, so on its own it would also pass if tailscaled never read an OS
base config at all. Register a snippet the way a DHCP client would, then
toggle accept-dns to force a reapply, and assert its nameserver becomes
quad-100's upstream.
SetAcceptDNS is the toggle: tailscaled has no reason to re-read the OS
config on its own, and this avoids rebooting the guest to change what the
OS resolver looks like mid-test.
Updates #20825
Updates tailscale/corp#44793
Signed-off-by: Brendan Creane <bcreane@gmail.com>
TestUserMetricsByteCounters is currently our top flake at http://flakes/
The test waited for a direct path between its two localhost nodes by
polling s1's status until CurAddr was set, then asserted that the
transferred bytes appeared in the path="direct_ipv4" counters. Both
steps were flaky:
- CurAddr is only reported while magicsock has a confirmed, currently
trusted UDP address for the peer, which depends on a heartbeat ping
and CallMeMaybe cycle that can lag arbitrarily under CI load. In the
CI failure logs the transfer itself went direct and the byte counter
assertions passed; the only failure was the polling helper's
t.Error.
- On a machine where localhost has both IPv4 and IPv6, magicsock can
pick ::1 (it prefers IPv6 on latency ties), so the transfer lands in
the direct_ipv6 counters instead.
- If no direct path is established yet, the transfer goes over DERP, whose
wire-byte total (relay framing, plus TCP retransmissions inside the
tunnel under load) can exceed any fixed tolerance; one flakestress
run counted 33% more than the payload sent.
Drop the direct-path wait and the hardcoded path label, and assert the
one invariant the test actually cares about: at least the number of
bytes sent must be counted across all path counters combined. There is
deliberately no upper bound, since the counters legitimately include
overhead, retransmissions, and the duplicate copies magicsock sends on
several paths while a direct path is being confirmed.
Verified with flakestress: the old test failed within 7 runs, while the
fixed test passed 33,521 runs with no failures.
See
http://flakes/analyze-test?name=tailscale.com%2ftsnet.TestUserMetricsByteCounters
Updates #deflake
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I4d8a9bfb9549ea9d8d33d3a3b55b4d03f9b9be78
On Windows, I could run tailscaled in the terminal over SSH only when
built with ts_omit_syspolicy, but that's kinda a sad workaround. This
is the alternative.
syspolicy registers a Registry policy store for the current user
whenever tailscaled isn't running as LocalSystem, and the first read
of that store takes the Group Policy read lock via
EnterCriticalPolicySection(FALSE). That call fails with
ERROR_ACCESS_DENIED when the user has no interactive logon session,
which is the case for a tailscaled.exe started over SSH (or WinRM,
psexec, a non-interactive scheduled task). The store's Lock method
returned the error, the reader failed to load, and LocalBackend
treated that as fatal:
ipnlocal.NewLocalBackend: syspolicy: LocalBackend failed to register
policy change callback: failed to get a store reader: Access is denied.
The GP lock is only there to keep reads of several settings consistent
while Group Policy is being applied; the store already documents that
reading the Registry without it is safe, and already skips it when
gp.ErrLockRestricted is returned during service start. Treat
ERROR_ACCESS_DENIED the same way: log it and read unlocked. Other errors
from the lock still propagate.
The optionalPolicyLock wrapper now holds the lockableCloser interface
instead of *gp.PolicyLock so the test can inject a failing lock.
Tested on a Windows Server 2022 VM: the test passes from an SSH session,
and a foreground tailscaled started from that session now runs and logs in
instead of exiting at startup.
Updates #21290
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I395f356730092299cf020a6787c56ab3b9ed6fe5
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
wintun-go loads wintun.dll only from the application directory and
System32. The MSI puts it there, but a plain "go build" tailscaled.exe run
from a terminal has no wintun.dll, and tstunNewWithWindowsRetries then
retried tstun.New for five minutes with nothing in the log but
tstunNew: backoff: 13 msec
tstunNew: backoff: 32 msec
because backoff.BackOff never logs the error it's given, and after the
timeout the function returned context.DeadlineExceeded, discarding the
real error.
Instead, log with helpful text if we detect that.
Updates #21290
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I09dca3317974a977deb9956657a7e36e9ec8c572
SetBufferSize sets the socket buffer to the requested size rather than
growing it, so on a host whose net.core.{r,w}mem_default is above the
7MiB the test asks for, the buffers legitimately shrink and the test
fails. Only assert that a buffer we asked to enlarge did not shrink.
Also log curSnd/newSnd, rather than the receive values twice, in the
SO_SNDBUF line.
Fixes#15994
Signed-off-by: aza <DevAza23@users.noreply.github.com>
Add, GetAll and Len all check for a nil receiver and document that they
do nothing, because a nil *RingLog is how callers represent a disabled
log: magicsock leaves endpoint.debugUpdates nil on iOS and Android to
save memory, and calls Add on it unconditionally.
Clear was the one method without that check, so it would panic on those
platforms. Nothing calls it on a nil log today, so this is a latent
footgun rather than an observed crash, but the inconsistency is easy to
fall into. Add the check and a test covering all four methods.
Updates #cleanup
Change-Id: Ic13693a27ad404f0e27906f4010980c9dafb7cc2
Signed-off-by: Aman Jain <jn_aman@yahoo.com>
Ignore malformed D-Bus NameOwnerChanged signals before reading their
body. A short signal previously logged an error but continued indexing
Body[0] and Body[2], which could panic the resolved manager run loop.
Also report the malformed body length correctly and add coverage for
malformed, stopped, and restarted systemd-resolved signals.
Updates #21270
Signed-off-by: alexchang <dragoonchang@gmail.com>
Cover process attribution, loopback filtering, socket churn, transport
fallback and recovery, and namespace consistency. Reuse the existing
kernel-regression skip helper for live tests and leave namespace setup
to the caller. Probe all diagnostic handlers before tests depend on them,
skipping known capability failures such as missing UDP diagnostics.
Add warm and cold backend benchmarks with held TCP connections and
listener scaling. Assert that diagnostic polls do not silently fall back.
Updates #10430
Updates #15492
Signed-off-by: JeremiahM37 <JeremiahM37@users.noreply.github.com>
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>
The map response reader now caps a single message at 256 MiB on the wire
and 1 GiB after zstd decompression. The server-chosen uint32 size prefix
previously let a malicious control server make us allocate up to 4 GiB
before reading any body bytes, and the decoded size was unbounded, so a
small zstd frame could expand into gigabytes of JSON. A 16 MB cap has
been hit by real production traffic before, so both limits sit far above
plausible legitimate sizes. The size-prefixed read moved into a
readMapResponseMessage helper, and the newer control/tsp path already
enforced both kinds of bounds; this brings the long-poll path it
replaces in line, with more generous limits for large tailnets.
ts2021.Client.Do additionally caps every noise response body with
httpbody.LimitSize, so a malicious or buggy control server can't make us
buffer an unbounded response. Client.Do shadows the embedded
http.Client's Do method, so register, set-dns, set-device-attr,
audit-log, all DoNoiseRequest consumers (webclient, tailnet lock, SSH
actions, id-token, feature queries), and the debug CLI get the cap
without per-call-site changes, and future noise endpoints get it for
free.
The cap lives in the new util/httpbody package so other HTTP clients can
adopt the same convention: LimitSize looks the size limit up from
res.Request's context (a Response knows the Request that produced it),
falling back to DefaultMaxSize, 1 MiB, when the context carries no
override. It is like io.LimitReader except that reads past the limit
fail with an error wrapping httpbody.ErrTooLarge instead of silently
truncating, and a body of at most the limit, including one of exactly
the limit, reads back without error: the wrapper probes for EOF once the
limit is exhausted to tell an exactly-at-limit body from an oversize
one. The per-request override, httpbody.WithMaxSize, is a context key,
so transports pick it up with no API changes; LimitSizeTo applies an
explicit limit ignoring any override. Repeated LimitSize or LimitSizeTo
calls replace the previous limit rather than compounding it, so a later
call can raise or remove the limit an earlier one set.
Responses that stream an unbounded number of individually bounded
messages disable the cap with httpbody.WithMaxSize(ctx, 0): the
/machine/map long-poll and control/tsp's map session, whose messages are
already capped per-message (by readMapResponseMessage and decodeMsg, and
by tsp's framedReader and boundedReader). Their non-200 error bodies are
not message streams, so those are capped with LimitSizeTo instead.
The tailnet lock /tka/init/begin, /tka/sync/offer and /tka/affected-sigs
responses can carry per-node key signatures or missing AUMs, which at
100,000 peers reach tens of MB, so they raise the cap to 512 MiB. The
per-response io.LimitedReader decoders that silently truncated those
responses at 1 or 10 MiB are removed: the transport cap is now the
single enforcement point, and it reports oversize bodies instead of
truncating them.
The /key fetch over plain TLS switches from io.LimitReader to
httpbody.LimitSizeTo, so an oversized response reports the problem
instead of producing a confusing truncated-JSON error.
Thanks to Ben Carman for the report!
Updates tailscale/corp#48187
Reported-by: Ben Carman
Change-Id: Ibf95e1ab9e4f0d7ef8866e8c26e62ed2a514455a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
When a controlclient receives non-keepalive netmap, it updates the controlknobs
based on the capability map of the self node. We added a new knob based on
tailcfg.NodeAttrCacheNetworkMaps in be2f554dd3, and this ensures we correctly
propagate the attribute to the knob when coming up from a cache as well.
Updates #12639
Updates tailscale/projects#27
Change-Id: I40d34053c9743757f9fddca715379fc63a9ae6a0
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
A tailnet peer can remotely crash tailscaled by sending a DERP-sealed
disco CallMeMaybeVia message with an all-zero ServerDisco key. The
decoder accepts the zero key, and the relay manager later hands it to
DiscoPrivate.Shared, which panics on zero keys. The sender only needs
to be a relay-capable peer in the victim's netmap.
Auditing the other DiscoPrivate.Shared call sites reachable from
decoded messages turned up the same bug on the relay server side.
AllocateUDPRelayEndpointRequest.ClientDisco is attacker-chosen: one
slot must match the sender's disco key, and the other can be zero. It
flows unchecked into udprelay.Server.AllocateEndpoint, which calls
Shared on both client keys and panics in its eventbus subscriber
goroutine. AllocateEndpoint now rejects zero client keys with an error,
which its only caller already handles by logging.
Thanks to Ben Carman for the report!
Updates tailscale/corp#48187
Reported-by: Ben Carman
Change-Id: Ifc0f64d8f63270100b22c06e9f759dce624ab811
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Tests that exec a fresh CopyTo copy of the tailscale/tailscaled
binaries occasionally failed with "text file busy" on GitHub
Actions, previously worked around with the retry loop in
awaitTailscaledRunnable.
The root cause: CopyTo's Linux hardlink fast path only works while
the built binary is still linked somewhere. The kernel refuses to
hardlink an inode whose link count is zero, even via the still-open
FD, so once the building test's TempDir (and every other test's copy)
has been cleaned up, later tests silently fall through to the
byte-copy path. That path writes the new copy from the test process
itself, and if another parallel test forks a child while the write FD
is open, the child holds the inherited FD (O_CLOEXEC only closes at
exec, not at fork) and a subsequent exec of the fresh copy fails with
ETXTBSY. This is golang.org/issue/22315. On macOS and the BSDs,
CopyTo always takes the byte-copy path, so every copy races there.
Fix it the way the syscall package documents: hold ForkLock for
reading across the copy so that no fork overlaps the lifetime of the
write FD. A standalone stress program reproduced the race in 86 of
200 execs under a fork storm and in 0 of 200 with the lock held.
Updates #15868
Updates #15865
Change-Id: I8a6be72826c9073a9187d7e6c90c8733d2dadb05
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Nothing reads ipn.Notify.NetMap anymore. The previous commit removed
its runtime (non-initial) emission, and every first-party client is
also off the initial one: the Win32 and WinUI GUIs and the Apple
bridge subscribe with InitialStatus or InitialState plus peer deltas,
Android no longer uses it, and the remaining in-tree subscribers that set
NotifyInitialNetMap (sniproxy and the kube helpers) only did so to get
the initial Notify.SelfChange and discarded the netmap that tailscaled
built, encoded, and shipped for them.
Delete the Notify.NetMap field and the NotifyInitialNetMap bit. The
bit value stays reserved under the name ObsoleteNotifyInitialNetMap and
ValidateNotifyWatchOpt rejects subscriptions that set it, like the
NotifyRateLimit bit removed in the previous commit. NotifyNoNetMap
remains accepted as a no-op because shipping GUIs still set it.
The blessed way to seed a watcher's view is NotifyInitialStatus, but
it unconditionally built O(peers) status entries, which is exactly the
waste this series is deleting for watchers that only care about the
self node. Size the initial status to the subscription instead:
Status.Peer is only populated when the watcher also set
NotifyPeerChanges or NotifyPeerPatches, since only peer-delta
subscribers need a peer baseline to apply deltas to. That matches its
existing first-party users (containerboot and the WinUI GUI both pair
InitialStatus with peer bits).
Migrate sniproxy and the kube helpers to NotifyInitialStatus: they
seed from InitialStatus.Self and react to the (ungated) runtime
Notify.SelfChange messages after that, so their initial message now
carries one PeerStatus instead of a full netmap.
Also drop doc comment references to LocalClient.NetMap, a method that
does not exist; on-demand fetches go through other LocalAPI methods
such as LocalClient.Status.
Updates #12542
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I242992a744c0ffd0be6f27e8c735aa69d5b23b5e
The WinUI GUI was the last consumer of the legacy Notify.NetMap field
on runtime (non-initial) IPN bus messages. It was converted to peer
deltas in tailscale/corp#47958, and the GUI and tailscaled ship
together as a unit on Windows, so tailscaled no longer needs to build
and emit full netmaps on the bus on any platform. Remove
goosGetsLegacyNetmapNotify and the code it gated. The initial netmap
(NotifyInitialNetMap) is unaffected; NotifyNoNetMap is now a no-op but
remains accepted for compatibility.
With no runtime netmaps left to rate limit, the NotifyRateLimit
subscription bit is meaningless, so remove the rateLimitingBusSender
machinery too. The bit value stays reserved under the name
ObsoleteNotifyRateLimit and ValidateNotifyWatchOpt now rejects
subscriptions that set it; previously it was only rejected in
combination with new-style delta bits.
Updates #12542
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ic6184ea549726de9d9d56f133aae21e53f62cbc5
The tsconnect wasm build still has osusergo and netgo since its early
6f5096fa6, copied from our linux static-linking build flags. They are
no-ops on js/wasm: there is no cgo resolver or cgo os/user
implementation to disable, so the pure Go paths are used regardless.
The omitidna and omitpemdecrypt tags (used by the wasm build and by
gocross for darwin and ios) were binary size reduction patches in our
github.com/tailscale/go fork, not upstream Go, and did not survive the
fork's per-release history reboots: omitpemdecrypt only ever existed
on the tailscale.go1.14 and tailscale.go1.15 branches, and omitidna
made it through tailscale.go1.17. Both have been unrecognized no-ops
since we moved to the go1.18 fork branch (927fc3612, March 2022, first
shipped in Tailscale 1.24.0).
Also stop passing tailscale_go explicitly: the tailscale/go fork's
cmd/go sets that tag itself as of 2026-03-31.
Updates #21250
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I10f1244f02a915943ca84e107aff8683b6e771b3
This change expands our fuzzing coverage in protocol and parsing logic. No issues discovered from this fuzzing. Wiring into oss-fuzz for continual coverage.
Updates https://github.com/tailscale/corp/issues/46608
Change-Id: I6b5218cb1103ccc5b957c512a10d87f637c4b6e5
Signed-off-by: Mike Jensen <mikej@tailscale.com>
DNATNonTailscaleTraffic in the nftables runner installs its exemption rule in the nat PREROUTING chain but matches on meta `oifname`, which routing has not yet selected at that hook. The exemption was therefore always true, so tailnet-arrived packets to the proxy's own address were DNATed to the egress target, forwarded back out tailscale0 and SNATed to the proxy's IP. This let any peer allowed to reach the proxy reach the target on all ports, bypassing tailnet ACLs.
This change matches on meta `iifname` instead, so traffic that arrived on the tun interface is exempt from the DNAT, mirroring the iptables runner's `"!" -i <tun>` semantics. Tailnet-originated packets now fall through to local delivery where the node's own ACL filters apply.
Reported by @KR-Ravindra
Fixestailscale/corp#47962
Change-Id: I1861a348b792ad4ae8e295447078a302f99d7d77
Signed-off-by: Mike Jensen <mikej@tailscale.com>
Two fixes for DNS names from a malicious control server, from Ben
Carman's security review:
dnsname.ToFQDN now rejects names containing whitespace or control
characters. ToFQDN previously checked label lengths only, so a search
domain like "evil.com\nnameserver 6.6.6.6" passed validation and was
written verbatim into /etc/resolv.conf by the direct DNS manager, where
the injected line became a real nameserver. The same shape existed on
Windows, where a CRLF in an ExtraRecords name injected lines into the
hosts file. This is deliberately not RFC 1123 hostname validation (see
the existing comment about issue 2024): labels may still contain any
byte that isn't whitespace or a control character.
dnsConfigForNetmap now drops search domains and split-DNS route suffixes
that ToFQDN rejects. It previously logged the error but appended the
zero FQDN anyway, and FQDN.WithoutTrailingDot panics on the empty FQDN
when OS DNS config is written, so one over-long domain from control
crashed tailscaled on every netmap until control sent a valid one.
Thanks to Ben Carman for the report!
Updates tailscale/corp#48187
Reported-by: Ben Carman
Change-Id: I8de40cafacfb66e2b863f097794c42f3ca5c2da9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
State keys written by dev-set-state-store are otherwise gated by their own handlers, for example serve-config requires a local admin before storing a _serve/<profile-id> key.
Writing such a key directly through dev-set-state-store skipped that check. Require`IsLocalAdmin` in the handler, the same check serve-config performs.
Credit to @johnnymiranda for reporting this issue.
Fixestailscale/corp#47886
Change-Id: Ie82f961b016f78895793d801929a4fa11ebf7fd8
Signed-off-by: Mike Jensen <mikej@tailscale.com>
Some customers rely on the ability for 4via6 subnet routers to expose
loopback and/or link-local addresses. In situations where the
administrator has deemed these to be safe, accept a list of allowed
addresses in an environment variable named TS_4VIA6_ALLOW_LOCAL.
Fixes#21019
Change-Id: I7b3fe863696ee67aa352c4bd11edcbc8836920e3
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Previously retry-after support was added to register requests in #19403,
in order to have more fine tuned client/control interactions in cases of
rate limiting.
This change expands that support to map sessions as well.
Updates tailscale/corp#47299
Signed-off-by: Evan Lowry <evan@tailscale.com>
Prior to 85bb5f8 mapSession was running directly as part of the
controlClient. That was changed previously to let discoKeys to flow into
the mapSession for TSMP learned keys, using the mapSession for filtering
of keys.
Since moving the TSMP key learning directly into the userspace engine,
and introducing the notion of two active disco keys for an enpoint, we
no longer need mapSession to run in a separate goroutine and have
separate paths of entry for data.
This commit effectively reverts the last changes that was previously
made in the controlClient to support TSMP.
Updates #20590
Signed-off-by: Claus Lensbøl <claus@tailscale.com>
We have visibility into what the gocacheprog is doing via its stats in
the github jobs, and its session on the server side, so this is just a
spammy log line that doesn't print anything very interesting. In
particular, cigocacher is started once for each package when you pass a
list of packages to testwrapper, so it prints many times for jobs where
we filter to a certain set of packages.
Updates #cleanup
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
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
* net/dns: export OSConfigurationReadWarnable
A natlab vmtest checks that a node is not reporting this warning. Exporting
the Warnable lets the test take the warning's text from it, instead of
keeping a copy of the wording that could stop matching without failing.
Updates #20825
Updates tailscale/corp#44793
Signed-off-by: Brendan Creane <bcreane@gmail.com>
* tstest/natlab/vmtest: add VM coverage for the openresolv DNS backend
dnsMode() picks one of five Linux DNS backends, and natlab could provision
only systemd-resolved and direct. Add a DNSOpenresolv mode and a VM test for
it, so the backend behind #20825 is covered.
No cloud image ships openresolv and a guest cannot download it, so its two
source files are vendored under testdata and installed with cloud-init's
write_files. openresolv's build is a set of sed substitutions with nothing to
compile, so resolvconf.go does the substitutions in process.
Updates #20825
Updates tailscale/corp#44793
Signed-off-by: Brendan Creane <bcreane@gmail.com>
---------
Signed-off-by: Brendan Creane <bcreane@gmail.com>
isAddressValid rejected all non-masquerade destination addresses
whenever any masquerade address was set for the peer. With a v6-only
masquerade pair, the peer's client still dials the v4 (native, not
masqueraded) peerapi URL, so every fresh peerapi connection got a 403.
The bug was masked by HTTP keep-alive: peerNode is snapshotted per
connection, so connections established before the masquerade was
configured kept validating against the old node view. It surfaced when
a newer tailscale/go toolchain started closing idle netstack
connections, forcing fresh peerapi connections and failing the
TestNATPing v6=true NAT subtests with 403s.
A masquerade address for one family says nothing about the other
family, so require a masquerade address match only for the address
family it applies to, and fall through to the self-addresses check for
the other family.
Fixes#21194
Change-Id: Idd2ed82b81e805b47b6aa6af4b5938984c6fa208
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Route packets to dedicated WireGuard, host, and loopback queues when
gVisor writes them. Drain each queue in a separate goroutine, preparing
the outbound path for batching and multiqueue WireGuard delivery.
Updates tailscale/corp#37878
Signed-off-by: Jordan Whited <jordan@tailscale.com>
TestUDPConcurrent closed its UDP connection as soon as the send loop
finished, and then required that at least one reply had arrived. With
GOMAXPROCS=1 the send loop runs to completion before the proxy's relay
goroutines are scheduled at all, so the close discarded every response.
The test failed on all 20 runs of "go test -race -count=20 -cpu=1".
Wait for the first reply, with a 10 second timeout, before closing the
connection.
This also makes the test catch the race it was written for more often.
With the fix in 0301c7493 reverted, "-cpu=2" reports the race where it
previously reported none.
Fixes#21182
Signed-off-by: Brendan Creane <bcreane@gmail.com>