Repository navigation
feat(ci): Bump to upstream v1.104.4 - #37
Merged
Merged
Conversation
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 tailscale#19707 Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
…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 tailscale/corp#46608 Change-Id: I0b95edcd0df946f723eea32f575c679214c0b202 Signed-off-by: Mike Jensen <mikej@tailscale.com>
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 tailscale#21072 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I1ccb77fa3c7a49532ea87dbdbf9e3340880ec94e
Updates #bump Change-Id: I5ad390397062bf9887962c34a8f3a047ea9586e9 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
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 tailscale#10602 RELNOTE: Prevent UPnP rule accumulation on MikroTik routers. Change-Id: Id925ed9cc3a3da6ecb06fc892f2a934addfac37b Signed-off-by: Jake Bailey <jacob.b.bailey@gmail.com>
…uilds (tailscale#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 tailscale/corp#46608 Change-Id: Ic6805854f76c597a5edff60674b5ea05b2d18ea9 Signed-off-by: Mike Jensen <mikej@tailscale.com>
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
… keys learned over TSMP directly to magicsock (tailscale#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: tailscale/wireguard-go#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 tailscale#20590 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
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>
…tailscale#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 tailscale#13038 Signed-off-by: Brendan Creane <bcreane@gmail.com>
freebsdRouter.Close ran "ifconfig tailscale0 destroy" while this process still holds the tun open: the engine closes the router before the tun device, and FreeBSD's tun(4) makes SIOCIFDESTROY sleep until the last descriptor closes. tailscaled therefore deadlocks against its own child ifconfig on every shutdown, and "service tailscaled restart" hangs forever: 51709 - I /usr/local/bin/tailscaled -port 41641 -tun tailscale0 ... 52455 - I ifconfig tailscale0 destroy 52403 1 I+ /bin/sh /usr/local/etc/rc.d/tailscaled restart Do only the PF cleanup at Close time. The interface left behind at process exit is destroyed by the next startup's cleanUp hook (router.HookCleanUp), which exists for exactly that and runs when nothing holds the device. Updates tailscale#5573 Change-Id: I7b739d948718bfb460240d2c0dd74658fc40ac82 Signed-off-by: Martin Minkus <martin.minkus@sonic.com>
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
Replace the allowed-IP-only peer callback result with wgcfg.PeerConfig. It carries allowed IPs and an optional pre-shared key through lazy peer creation and active peer synchronization. Update wireguard-go for the new peer PSK APIs. Updates tailscale/tailcat#84 Change-Id: Iacd9d2c74b0b64d690f3cbdf93918686ac6076d7 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Once the pkg-types script has generated pkg.d.ts and checked them against the tsconfig.json and node_modules in cmd/tsconnect, copy them into the pkgDir so they get bundled into the package. Updates tailscale#19707 Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
) Upates to controlClient learned keys was logged as coming from TSMP. Also, nil changes on nil keys were not filtered. Updates tailscale#20494 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
Rename the tailcat job added in 6c06e00 to heads-up-hypothetical-tailcat-build-using-this-commit and print an explanation on failure: the job builds tailcat against tailscale.com at this commit rather than the version tailcat pins, so a failure doesn't break tailcat until it bumps its dependency. It only matters if the API or behavior breakage is unexpected. People were misreading failures as something more urgent. Updates tailscale/corp#24454 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I3f8a1c92d47e60b5a2e9c14f7d08b361
This commit bumps the wireguard-go dependency to incorporate changes to the packet memory model and the tun.Device.Read and conn.ReceiveFunc I/O interfaces. It updates their implementations accordingly. These changes improve throughput in all measured benchmarks and reduce peak RSS in six of eight cases. The two regressions will be addressed in a follow-up commit that reduces peak RSS below the baseline measured at 1e69418. That work is kept separate to simplify review. The following throughput and peak RSS benchmarks were performed with iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8). The UDP benchmarks did not use UDP GSO on the sender, so they were roughly equivalent to single packet I/O through wireguard-go. TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP streams. Throughput (Mb/s) Test 1e69418 After Change TCP/1 10,371 11,354 +9.5% TCP/128 7,886 8,404 +6.6% UDP/1 2,111 2,853 +35.1% UDP/128 1,747 2,235 +28.0% Peak memory (VmHWM, kB) Test Side 1e69418 After Change TCP/1 TX 98,240 52,596 -46.5% RX 287,748 73,384 -74.5% TCP/128 TX 101,196 52,812 -47.8% RX 290,420 63,620 -78.1% UDP/1 TX 58,864 160,840 +173.2% RX 137,516 49,900 -63.7% UDP/128 TX 66,148 116,096 +75.5% RX 154,384 56,556 -63.4% Updates tailscale/corp#46716 Updates tailscale/corp#22467 Updates tailscale/corp#36989 Updates tailscale/corp#37878 Signed-off-by: Jordan Whited <jordan@tailscale.com>
When applying netmap deltas, nodeBackend evicted its index entries (nodeByAddr, nodeByKey, nodeByWGString, nodeByStableID, nodeByName) derived from a node's last-known value without checking that the entry still pointed at that node. Control can reassign a churning ephemeral peer's Tailscale IP (or MagicDNS name) to a newer peer and deliver the new peer's upsert before the old peer's removal, either in an earlier MapResponse or reordered within one batch by the NodeID sort in netmap.MutationsFromMapResponse. The removal then wiped the new owner's entry. The peers map itself stayed correct in every ordering, so WireGuard kept the peer and handshakes succeeded, but WhoIs lookups by IP failed until the next full netmap rebuilt the indexes. On App Connectors that surfaced as "peerapi: unknown peer" and refused DNS connections from affected clients, with a toggle of Tailscale (forcing a full netmap) as the only recovery. Make every index eviction conditional on the entry still mapping to the node being removed or replaced, and add a regression test covering the cross-batch, intra-batch, and upsert-eviction orderings. Also add an end-to-end test in tstest/integration showing that a MapResponse reusing an address is handled incrementally rather than as a full netmap, and that LocalBackend.WhoIs still resolves the reused address afterwards, which is the lookup PeerAPI makes before it accepts a connection. Updates tailscale/corp#47435 Co-authored-by: Brendan Creane <bcreane@gmail.com> Signed-off-by: Brendan Creane <bcreane@gmail.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I3f8c2a9d41e07b6a5cd2e94f78b013c6ad2f5e91
Refresh the expiry timer netmap from the live peer state before reinstalling it, preventing delta updates from being rolled back. Updates tailscale/corp#47686 Change-Id: Idc738acea82bab5a8ba772084a41e55b38a06bcc Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
A full netmap carries only the profiles of users with a currently visible peer (netmapForResponse), and nodeBackend replaces its live profile set wholesale on every full netmap install. A full netmap that arrives while a user has no visible peer therefore drops that user's profile downstream. When a peer of that user later returns as an incremental upsert, control does not resend the profile, because MapResponse.UserProfiles has carried only new or updated profiles since mapver 5. The upsert indexes the node by address and key, so WireGuard admits its traffic, but WhoIs then fails one step later at the user profile lookup, surfacing as "peerapi: unknown peer" until the next full netmap. It is a second, independent cause of the symptom fixed by the recent index eviction change. mapSession.lastUserProfile holds the profile the whole time, so when handling a response incrementally, also deliver the profiles of upserted peers' users (and sharers) from that store, before the mutations that reference them. Updates tailscale/corp#47435 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I9c4b2f6a8e0d47d3b1a5c2e7f4098d61b23a7c50
…id binaries Android doesn't have /etc/resolv.conf. This causes problems for people running GOOS=linux binaries (or GOOS=android binaries without cgo, so they don't use Android's bionic libc) in Termux, adb shell, etc. (Go binaries built with cgo use bionic on Android: golang/go#10714) 16 years ago when I was on the Android team I added a system-wide DNS cache (dnsproxyd) and made bionic query that, so each Android app wasn't doing its own DNS resolution. That interface was never meant to be stable, and I thought that code would be surely dead by now 16 years later, but apparently it lives on, and is more stable now: both empirically (time, ossification?), and because of how Android's split system updates work nowadays, the dnsproxyd lives on the other side of bionic, so they seem to keep it pretty stable. The old bionic<->dnsproxyd APIs I added 16 years ago are still there, but 8 years ago it got some additional APIs to query by a DNS packet instead. So use it! If we find ourselves on Android and without libc access (and because we don't want to pull in ebitengine/purego with all its side effects), just query the DNS server like bionic does. This can be disabled in Linux binaries with ts_omit_androiddns. Old links: LineageOS/android_system_netd@007e987 https://android.googlesource.com/platform/system/netd/+/007e987fee7e815e0c4bc820f434a632b7a69a9d ("DNS proxy thread in netd.") aosp-mirror/platform_bionic@a1dbf0b https://android.googlesource.com/platform/bionic/+/a1dbf0b453801620565e5911f354f82706b0200d ("DNS proxy: the start. proxies getaddrinfo calls.") Back then I found it cleaner to proxy at the getaddrinfo level rather than speak in terms of DNS packets. The raw-packet resnsend command I use here came eight years later, added in November 2018 for Android 10's android_res_nsend NDK API: LineageOS/android_system_netd@c0c818f https://android.googlesource.com/platform/system/netd/+/c0c818f448efa90ab1f9b1733fb86c5e22fb894c ("Add resNetworkSend cmd in DnsProxyListener") Android 10 (codename Q, API level 29, released September 2019) is therefore the minimum OS version for this to work. I verified this against the DnsResolver module on an Android 13 emulator with SELinux enforcing, from the shell UID, with both a pure Go GOOS=android binary and a static GOOS=linux binary: raw queries, NXDOMAIN handling, the runtime Android detection, and a tailcat binary reaching DERP with lookups visible in the daemon's logcat output, some served from my 2010 DNS cache. Updates tailscale#21129 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I0d63763e255a077e4e5745b3e64ba0d78dab6d69 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
…le#21090) Move the container image build logic out of the e2e test setup into cmd/k8s-operator/e2e/internal/build, with a thin CLI wrapper at cmd/k8s-operator/e2e/build, so CI can publish images once per commit and fan out into multiple test jobs that consume them. The command skips images that already exist in the registry, so a retried run doesn't fail if the registry is immutable. Also adds support for the test harness leveraging workload identity federation credentials so we can use GitHub's token issuer for the test code itself, and AWS' OIDC provider for the operator, and avoid using any secrets in CI. Updates tailscale/corp#46577 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
…igurable (tailscale#20991) The PeerRelay resource hardcoded UDP port 41641 in the generated Service, the tailscaled config, status.endpoints and the advertised static endpoints. That breaks deployments where the load balancer address or port is not what peers can actually reach, e.g. behind NAT. Add spec.service.port to choose the UDP port, and spec.staticEndpoints to advertise extra address:port pairs alongside the discovered load balancer endpoints. A replica whose only endpoint is a static one counts as addressed, and a static entry's port wins when it names an address the load balancer already provides. The e2e tests now supply static endpoints, which lets them assert the PeerRelayReady condition goes True in kind, where no cloud controller ever gives the LoadBalancer Services an address. Fixes tailscale#20821 Change-Id: I463012cd447c81c2849fa653fa18eb82662aaf4f Signed-off-by: David Bond <davidsbond93@gmail.com>
Previously the DERP handler served each connection for its lifetime on its hijacked connection's net/http handler goroutine. That goroutine's conn.serve stack frame kept the dead HTTP/1 server state reachable for the whole DERP connection: the http.conn and its 4KB bufio.Writer (hijack hands over c.bufw but conn.serve still references it, so derpserver returning it to its flush pool never made it collectable), the 4KB bufio.Reader, the upgrade *http.Request with its parsed headers, and the request context chain. The goroutine also kept the stack growth from the TLS handshake and HTTP request parsing. Instead, hand the connection off to a new goroutine and return from the handler (ala tailscale/corp@dc09e27aef711d), letting all the HTTP upgrade state be collected. Give Accept a smaller 1KB frame reader, draining and releasing the hijacked reader if it contains buffered bytes from a fast-start client, and a nil bufio.Writer so writes go through pooled buffers held only for the duration of a write instead of a per-connection buffer. Because the handler now returns at handoff time, cmd/derper's gauge_derper_tls_active_version decrement can no longer be deferred to handler return: intercept Hijack in the TLS metrics wrapper and, for hijacked connections (DERP, its WebSocket flavor, and CONNECT), decrement the gauge once when the hijacked connection closes, restoring the gauge's connection-lifetime semantics. Teach derpserver's TCP RTT stats to unwrap the close-hook conn so they still find the underlying *net.TCPConn. Also soften the UntypedHexString deprecation notices in types/key to warnings: the untyped hex string format is the DERP wire protocol's key encoding, so these call sites are legitimate and permanent, and a Deprecated marker just makes them light up in editors and linters. The cautionary text about the format's risks remains. Measured with 100,000 idle TLS DERP connections on linux/amd64: standing memory drops from 55.6KB to 32.6KB per connection (-41%). Updates tailscale#21064 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: If05a0c6ea79134807e9e8872861db216
Android denies app UIDs both NETLINK_ROUTE (golang/go#40569, tailscale#2293) and /proc/net, so net.Interfaces always fails and netmon.New errors out before a standalone binary can do anything. The Android app solves this from Java via netmon.RegisterInterfaceGetter, but raw binaries run under Termux or a rooted shell have no Java to lean on. This is the second half of tailscale#21129, following the androiddns feature. I added a netmon fallback hook, consulted only when no interface getter was registered and net.Interfaces failed, and a new androidbin feature (ts_omit_androidbin) that implements it: report a single synthetic interface whose v4 and v6 addresses come from asking the kernel to route an outbound UDP socket, which sends no packets and is permitted in the app sandbox. That's enough for magicsock to discover local endpoints. The fallback also requires runtime evidence of Android (GOOS=android, or /dev/__properties__ existing for GOOS=linux binaries running under an Android kernel), so it's inert on regular Linux. The androidbin feature also blank imports androiddns, and fixes a third gap I found while testing: GOOS=linux binaries on Android have an empty CA root pool, because Go's unix root loader doesn't know Android's /system/etc/security/cacerts (the GOOS=android loader does), so all TLS verification fails. On Android it points SSL_CERT_DIR there unless the user already set it, as Termux's ca-certificates package does. The feature is on by default in tailscaled builds on Linux and Android via condregister, and deliberately not linked into tsnet by default; tsnet apps and other programs opt in with a blank import of tailscale.com/feature/androidbin. I verified on an Android 13 emulator with SELinux enforcing, running GOOS=linux static binaries under the app UID (run-as), where net.Interfaces fails with the exact netlinkrib permission denial from the issue: netmon.New succeeds with the synthetic interface, and a tailcat binary importing this feature does DNS via dnsproxyd, fetches its DERP map over TLS using the Android cert store, completes STUN, selects a DERP region, and prints its address. Updates tailscale#21129 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: If7dbcbb825ecd24bcf6d9d64b1e334dd00700d51
Instead of opening all DNS queries with conn25, only permit clients to access domains of the apps that they have permission to use. Previous changes ensure that clients report which app they are making a DNS query for, simplifying the check. Updates tailscale/corp#40076 Updates tailscale/corp#47585 Change-Id: I40fee220b3f190b2a0db9b3e6cc79f323ef16b73 Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
Add a new modular dnsresolvecache feature that records every successful DNS resolution from net/dnscache as a JSON file per hostname in $statedir/dns-cache/, so a later boot with misconfigured DNS can still find last-known-good IPs for critical hostnames like the control plane. Files are rewritten only when their contents change, so a file's modification time records when the answer last changed. When regular DNS resolution fails, the disk cache is now consulted before the DERP-based bootstrap DNS in net/dnsfallback. This is the first step toward removing the DERP-based mechanism: new clientmetrics (dnscache_disk_fallback_hit, dnscache_disk_fallback_miss, dnscache_derp_fallback_ok, dnscache_derp_fallback_dial_ok) will tell us when the DERP path no longer fires in the fleet and can be deleted. The feature is linked into tailscaled by default (omittable with ts_omit_dnsresolvecache) and is not included in tsnet. Updates tailscale#21028 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I59228cbf68e3b48dfb1215cdd12bd8166ab14034
…connection-rejection diagnostics Adds an opt-in, in-memory aggregator of recent connection-rejection events (TSMP rejects received from peers, outbound TSMP rejects we emit on ACL-blocked inbound flows, and pendopen timeouts) keyed by (direction, proto, peer-address, reason). The aggregated data is exposed over a new debug-rejects LocalAPI endpoint and a GET /debug/rejects c2n endpoint, intended for future GUI/CLI consumption when diagnosing why a connection failed. Architecture: - net/connreject holds the data types and a per-LocalBackend Aggregator (LRU-bounded, default 256 entries on desktop / 32 on mobile, per direction). - feature/connreject is a self-registering ipnext.Extension that owns one Aggregator per LocalBackend, installs note callbacks on the tundev and engine, subscribes to OnSelfChange to flip the runtime gate, and serves the LocalAPI/c2n endpoints. - wgengine.Engine and *tstun.Wrapper each gain a SetConnRejectNote setter; data-plane sites use a single atomic.Pointer load + nil check, so the cost when no consumer is installed is one MOV. Gating: - Compile-time: ts_omit_connreject build tag (standard feature/buildfeatures + condregister plumbing). Trims ~41 KB. - Runtime: nodecap.ConnReject node attribute, off by default at the control plane. May be removed once the feature is enabled by default. Updates CapabilityVersion to 146 (clients understand nodecap.ConnReject and can serve GET /debug/rejects). Adds Proto/Src/Dst accessors on flowtrack.Tuple (used by pendopen to construct events without exposing the tuple's internals to the aggregator). Updates tailscale#1094 Updates tailscale#14802 Change-Id: I83e8f24a7e66fa2d158d128bd25fbe851134941b Signed-off-by: James Tucker <james@tailscale.com>
As of Linux 6.2 (torvalds/linux@f1f1f25), the stat size of the /proc/self/fd directory is the number of open file descriptors, so CurrentFDs no longer needs to walk the directory on modern kernels. The walk is O(n) in the number of open file descriptors, which adds up on servers like derper that hold many connections and report this metric on every scrape: at 10k open fds, the walk takes about 2.2ms while an fstat takes a constant 300ns. We fstat a directory file descriptor held open for the process lifetime rather than calling stat with the path, because syscall.Stat copies the path string to a NUL-terminated buffer on every call and the metric must remain allocation-free. Kernels older than 6.2 report a size of zero, in which case we fall back to the directory walk. Updates tailscale#2784 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I1742db2ae1938c77f9b7d137b167ab4a2b6b6167
Android has no /var, so DefaultTailscaledSocket fell through to the cwd-relative "tailscaled.sock": the daemon created its socket wherever it was started and the CLI looked for one wherever it was started, and the two only agreed by coincidence (reproduced on Termux in tailscale#21161). Return os.TempDir()/tailscaled.sock on Android - the shared per-app temp directory, following the same pattern as the existing QNAP /tmp default - so daemon and CLI agree on an absolute path. Fixes tailscale#21161 Signed-off-by: breken <hi@breken.ai>
sftpSubsystemSession waited for the stdout copier's done channel but not the stderr copier's. The stderr copier carries the recording-policy refusal notices. The deferred channel close could cut it off mid-drain. The refusal text never reached the test's buffer. Both copiers are now waited on before the channel closes. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit b71e0f3)
… bound ListenSSH now documents its session trust model and takes an authorization callback. A session is accepted from any peer that the tailnet ACL's TCP rules allow and that resolves with WhoIs. The server performs no SSH client authentication, does not map the peer to a local user, and does not record the session. The application is responsible for what each session does. Set Server.SSHAuthCallback to reject unwanted sessions before they reach Accept. A rejected session is refused at open with exit status 1 and is never yielded to Accept. The callback is plumbed through LocalBackend.ListenSSH and HookListenSSH. With no callback, behavior is unchanged. The listener's SSH server runs session channels only, with no port forwarding. Closing the listener closes the accepted connections. - DisablePTYEmulation runs on every session, so a PTY session passes data through unchanged instead of translating "\n" to "\r\n". The translation corrupted binary data read through the net.Conn interface. Window changes are drained for every session, even when the application never calls Pty, because gliderssh writes window changes from the session request loop and an undrained channel blocks the session's requests. Pty drops window changes when the application is not reading them. - The handshake is bounded by TS_SSH_LOGIN_GRACE_TIME, default 120 seconds, the same as sshd's LoginGraceTime. - TS_SSH_IDLE_TIMEOUT bounds connections with no SSH traffic. It is 0 by default, and zero applies no bound, so quiet but live sessions are not closed by default. Both timeout knobs accept a Go duration such as "90s" or a bare number of seconds. - Accepted sessions wait for Accept in a bounded queue. When the queue is full, further sessions are refused, so an application that stops calling Accept parks only a bounded number of sessions. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit a1e51f4)
Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 5f40a06)
New self-contained code for the per-connection privileged ssh-monitor. No existing behavior or wiring changes. Later commits wire the forwarding and session paths onto it. rpcFrame is the newline-delimited JSON wire struct with request IDs. It covers dial, listen, exec, signal, and ping requests, ok and err replies, and unsolicited exit notifications. rpcConn is the framing codec over one socketpair end. Reads go only through ReadMsgUnix with a CmsgSpace(3*sizeof(int)) OOB buffer. A plain Read or an undersized buffer silently drops ancillary data, and the kernel closes the dropped descriptors. A stream read never returns data beyond an SCM_RIGHTS-carrying message, so a descriptor arrives with the first chunk of a split line and survives reassembly. Writes attach descriptors to the first byte of a line and never to an empty payload. A short write resends the remainder as plain data, because resending the ancillary data would duplicate the descriptors. Received descriptors are marked close-on-exec at parse time, by the codec rather than by the stdlib's platform-dependent handling. Herald attribution validates a whole read's herald batch before queueing any set for a frame, so a failed batch closes its sets exactly once and never double-closes a set already queued for a frame. Lines are capped at 256 KiB, and NUL bytes and frames carrying more descriptors than their op declares are rejected on receive. A read timeout returns os.ErrDeadlineExceeded and preserves any partially received line, so the monitor's poll loop can sweep between reads without losing the frame. A read timeout is the only error that preserves state. Every other error closes all held descriptors and clears the buffer. sendFds, recvFds, and wrapUnixConn are shared by the monitor and client sides. wrapUnixConn consumes its fd, because os.NewFile's finalizer cannot be neutralized from outside. Tests exercise ping and fd round-trips with liveness and FD_CLOEXEC checks, multi-fd frames, kernel framing pins for first-chunk ancillary delivery and undersized-OOB MSG_CTRUNC rejection, 64 KiB-plus split-line reassembly under a shrunken SO_SNDBUF, oversized and NUL rejection, the short-write resend path, the read-timeout state-preservation contract, and 50-way multiplexed request/reply traffic. The monitor scrubs every descriptor from 3 up to close-on-exec at entry and again before each exec. Go's exec closes nothing beyond the descriptors it passes, so a single non-close-on-exec fd anywhere in the spawner's table would otherwise reach every child the monitor execs. close_range(CLOSE_RANGE_CLOEXEC) does the sweep in one syscall on kernels 5.11+; older kernels scan the self-fd directory. A new test injects a non-close-on-exec pipe at spawn and pins the clean-child contract. spawnRPCChild takes the child's environment explicitly: a nil env gives the child an empty environment, never the spawner's. The monitor child is spawned with the daemon's environment; user-identity children are spawned with none, so the daemon's environment never reaches a child running as the connection's user. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit ed79035)
…/listen ops Add the one-shot "be-child ssh-fwd" children that perform forwarded socket work, and the monitor and client plumbing for the socket ops. The conn and session flows do not use the ops yet. The next commit routes them. - fwdchild.go: a child re-execed by the monitor, born unprivileged at clone. The monitor applies the connection's bound identity as SysProcAttr.Credential, so the kernel's DAC sees the connection user's permissions. It dials a unix socket, or creates one with umask-at-bind mode and optional unlink-if-owned, or binds a TCP listener, or creates the agent-forwarding socket in a user-owned directory. The result returns to the monitor over an inherited socketpair as one status line with at most one descriptor attached. - monitor.go: handleDial and handleListen run the ops on one-shot children with a spawn-to-result deadline, monFwdOpDeadline. On expiry the child's group is SIGKILLed. killChild reaps such a child with a bounded non-blocking wait. A child left in uninterruptible sleep is abandoned in its own group rather than blocking the loop goroutine. The dial and listen placeholder replies are gone, and their not-implemented test with them. - monitor_client.go: Dial, Listen, and ListenAgent over the existing request-ID plumbing. - incubator.go, monitor_linux.go, monitor_other.go: registration and per-platform spawn attributes for the fwd children. Tests drive real children across uid boundaries. They cover dial and bind round trips, unix and TCP listens, agent-listen directory hygiene, unlink-if-owned, peer-credential assertions, and the no-identity-keys-on-the-wire shape of every request. Forwarding children spawn with an empty environment through spawnRPCChild: they take everything from argv and the result socket, and the daemon's environment must not reach a child running as the connection's user. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit de16943)
Route tailssh's forwarding through the per-connection ssh-monitor, so on unix forwarded sockets are dialed and bound by children with the authenticated local user's credentials, never by root tailscaled. - unix -L, direct-streamlocal: the lexical allowlist check stays in tailscaled as coarse policy. The allowlist is home, /tmp, and /run/user/<uid>. Relative and abstract paths are rejected. The dial then runs as the user through the monitor. The EvalSymlinks re-validation is dropped, and the kernel's DAC is the boundary. The allowlist is capped. A home directory that cleans to "/" or is empty would prefix-match every absolute path, and such an entry is dropped instead. Paths at or beyond the sun_path capacity reject lexically rather than dying at bind or dial with a generic EINVAL. - unix -R, streamlocal-forward: the listener is created by a user-cred child with umask-at-bind 0600 and unlink-if-owned. Closing it does not unlink the socket, and the stale file on cancel is accepted. - TCP -R, tcpip-forward: a tailssh-owned handler replaces gliderssh's ForwardedTCPHandler. The bind goes through the monitor's listen op with the local user's credentials, so privileged ports fail by kernel construction for non-root-mapped users. Root-mapped users are allowed, as in OpenSSH. Bind addresses are loopback-only unless TS_SSH_FORWARD_GATEWAY_PORTS is set, the equivalent of sshd's GatewayPorts. - agent forwarding: the socket and its containing directory are created by a user-cred child, a 0600 socket in a 0700 user-owned directory, instead of a root-run MkdirTemp, Chmod, and Chown sequence. Session-end cleanup is unchanged. The monitor is spawned lazily on the first forwarding or agent need, from the tailscaled binary, and closed when the SSH connection ends. closeFwdMonitor latches a closed flag under the fwd lock, so a caller racing the teardown is refused by fwdMonitor, or reads the current state without spawning through fwdMonitorIfAny, instead of respawning a monitor nobody ever closes. plan9 keeps today's behavior through a build-tagged seam, fwdmonitor.go and fwdplan9.go. There is no monitor on plan9. Unix forwarding is rejected there, and reverse TCP forwarding keeps the gliderssh handler with the mayReversePortForwardTo gate. Tests cover conn-level callbacks against a real monitor and real one-shot fwd children: allowlist rejections, user-owned 0600 sockets, a foreign sticky-dir socket not unlinked, loopback, gateway-ports, and privileged-port TCP binds, agent directory hygiene and cleanup, and TS_SSH_DISABLE_FORWARDING coverage, plus refused respawn after teardown. The unix-fwd root-dial regressions cover a static in-allowlist symlink to a root-owned 0600 canary, which must fail permission denied as the bound user, and a dir-swap race, which must never serve the protected canary. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit eba00ab)
Launch incubator sessions through the per-connection monitor, passing stdio and the optional environment pipe via SCM_RIGHTS. tailscaled retains the parent I/O descriptors. Keep direct launch when no tailscaled binary is available, and on Plan 9. Wait for dispatched exec replies even after session cancellation so the session retains ownership of any child that starts. Publish exit status through persistent completion state. Identify monitor children by PID and exec request ID, and validate both when signaling. Remove raw-PID signaling after monitor teardown. Normalize incubator identity flags and reject alternate spellings that could override them. The monitor RPC caller remains trusted to supply the executable and other arguments; this is not a privilege boundary against tailscaled. Pass default TERM and locale values through the environment pipe, preserving last-value-wins defaults and policy-forwarded precedence. Use a separately owned close-on-exec PTY descriptor for resizing. Add regression tests for launch cancellation, exit delivery, signal identity, descriptor ownership, and environment handling. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 79d5abe)
Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit df83989)
Re-evaluate authenticated connections on policy updates and at session and forwarding gates. Close connections whose grants are revoked or narrowed, including forwarding permissions, user identity, duration, AcceptEnv, and effective recording requirements. Recheck rule-derived AcceptEnv and recording configuration for delegated connections too. Compare recorder lists with duplicate counts preserved so duplicate entries cannot hide a change of recording destination. Treat recording-failure changes in either direction as material, including fail-open to fail-closed transitions. An inconclusive user lookup retains the previous identity but does not skip other policy checks, including for `"*": "="` mappings. An absent account or a changed username, UID, or primary GID revokes the connection. Preserve explicit getent failures when the os/user fallback cannot find the account, instead of treating every lookup failure as account deletion. Numeric UID mappings use LookupId in the fallback, not Lookup by name. Known limitation: NSS can report a directory-only account as not found (getent exit status 2) during an sssd/LDAP outage. If the fallback also reports the account missing, this is indistinguishable from deletion and still revokes the connection. This availability tradeoff remains accepted for now; explicit command failures, timeouts, and malformed output do not by themselves establish that an account was deleted. Reject forwarding requests using the latched Allow bit before doing a policy recheck. Forwarding gates still re-evaluate policy, but use the latched account identity rather than spawning getent for every forwarded connection (including each SSH -D request). Account identity is rechecked at authentication completion, policy updates, rule expiry, and session gates. Re-arm rule-expiry timers under the policy recheck lock and prevent re-arming after teardown. Add connection-level login grace, extending it during delegation and stopping it only on SSH authentication success, not on unsigned key queries or partial authentication. Close the raw transport and cancel delegated authentication on teardown. Add regression tests for these transitions, wait for the auth-completion recheck before test policy flips, and release stalled recorder handlers even on test failure. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit d75f04c)
Allocate the exclusion set before adding client-supplied usernames. A true Exclude entry otherwise panics when SSH is enabled. Exercise the c2n handler with true, false, absent, and unknown exclusions, and cover the result cap and disabled-SSH behavior. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> Change-Id: I1d9faf36e5809ccff888fc402329652e8b7d88c8 (cherry picked from commit 5cb033d)
Cancel fail-closed sessions before sending recording upload failure notifications. Bound each notifyControl request to five seconds, including recorder connection failures on the synchronous session startup path. An unresponsive control request must not keep an unrecorded session running or indefinitely delay rejection or fail-open startup. Keep the timeout local to each notification, independent of the session and upload lifetime, without adding another goroutine. Test blocked startup notifications in both rejection and fail-open modes, and use virtual time to cover earlier and simultaneous cancellation/deadlines. Send SFTP recording rejection diagnostics to stderr, preserving the binary protocol on stdout and exit status 254. Add real SSH/recorder tests for fail-closed and fail-open behavior with blocked notifications, and an SFTP client regression test. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> Change-Id: I270d624975a6a9115502a4915ee5ff28a67d714b (cherry picked from commit 84ca98e)
Keep the dialed service socket in the credentialed forwarding child and return a private socketpair endpoint instead. Passing the service socket to tailscaled preserves connect-time peer credentials but causes payload SCM_CREDENTIALS to identify the root daemon. Keep dial children alive for bidirectional forwarding. Return a lifetime pipe alongside the data socket so full closure or a discarded dial reply ends the relay independently of data EOF. Preserve half-closes and the monitor's descriptor acknowledgment/teardown ownership for both fds. Listener children retain their existing descriptor handoff behavior. Handle local Unix forwarding channel closure independently of the SSH connection: the channel request stream closes on CHANNEL_CLOSE, not CHANNEL_EOF. Stop waiting for service output while giving queued client payload a bounded drain in each forwarding stage, so a silent or non-reading service cannot retain the relay indefinitely. Test relay exit/reaping with silent services, discarded replies, queued payload and backpressure, including end-to-end SSH channel closure while the transport stays connected. Fix a canary test's unsynchronized marker assertion to wait for the reporting goroutine. Add a root-run cross-UID SO_PASSCRED regression test and lifecycle coverage for both half-close directions and monitor shutdown with an idle relay. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> Change-Id: I54b8ddf02b9d82434e474fea2ec283c0e39fab3e (cherry picked from commit 6072693)
Drain any stale buffered size before sending the latest window update. The adapter is the only sender to a one-slot channel, so the final resize is retained even if the consumer races with the drain. Cover late and slow readers over a real SSH session with deterministic synctest synchronization. Keep the final two resizes unread to exercise replacement of the buffered size. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com> Change-Id: I8c077114a6d4765c4208d42a22f384c82f73f75c (cherry picked from commit 1232322)
Route direct-tcpip channels, used by TCP -L and SOCKS -D forwards, through the per-connection monitor after the existing policy checks. Resolve names and create/connect TCP sockets in a child running with the authenticated local user credentials instead of the root daemon. Hand the connected TCP descriptor back through the existing acknowledged fd protocol. TCP socket ownership survives the handoff, so UID-based firewall policy sees the mapped user. TCP has no per-message SCM_CREDENTIALS and needs no persistent relay: the child exits after handing off the socket. Bound resolution/dialing by the monitor forwarding deadline. Share the channel-close-aware forwarding copy loop with Unix forwards, retaining bidirectional half-closes and bounded draining on full close. Plan 9 retains its existing in-process forwarding path. Add root-run IPv4/IPv6 socket-owner regression tests, hostname and half-close coverage, policy/disable gates, invalid/refused dial handling, and a pin that the child is reaped while its socket remains usable. Update the TCP policy revocation fixture for child spawning and synchronize its assertion on transport shutdown. Updates tailscale/corp#50380 Change-Id: I89159fd31e75c22446096587471afcd2bb4e78d4 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 87f3b61)
Check shutdown and register initialized connections under the same lock. Reject a connection if shutdown ran during its construction, stopping its login-grace timer and closing the transport. Also close the transport when connection construction fails. Cover shutdown before and during connection construction without starting an SSH handshake. Updates tailscale/corp#50380 Change-Id: I634036a7e073b24d7ff80d6a6d0ae991c40f3c69 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 617c38d)
Describe exit statuses, output ordering, and descriptor ownership directly. Keep the Darwin descriptor acknowledgment rationale in the protocol documentation instead of repeating it at each handoff. Clarify that the exec spawn limit is checked after Start returns, not an interrupting deadline. Remove claims that session exit statuses are unique or that unreaped children must be uninterruptible. Updates tailscale/corp#50380 Change-Id: I613b83ac2ae92d7bf8a582c4e4b326ebbe55852d Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 2c1cea5)
Move lazy monitor startup, credentials, and cleanup into connmonitor.go and rename the connection state and accessors. The monitor owns session processes as well as forwarding children. Move the session exit registry alongside session monitor handling and keep the Plan 9 stub in a matching file. Update callers, tests, and diagnostic prefixes without changing monitor lifecycle behavior. Updates tailscale/corp#50380 Change-Id: I760fa009176491da426639615907d129b68026fc Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit e3a4c41)
Let fetchSSHAction own the per-fetch deadline and pass the connection cancellation context directly from clientAuth. Remove the duplicate timeout and its deferred cancellation inside the action loop. Derive the longer transport watchdog from the fetch timeout, keeping both durations unchanged. Use virtual time to test the fetch deadline across retries, earlier parent deadlines, and parent cancellation. Verify that a successful fetch releases its request context. Updates tailscale/corp#50380 Change-Id: I185958ca19566f04cbd68f33e4b1dad3d18aebb7 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit f5fdb18)
When the ssh incubator runs the shell itself instead of using `su` or `login`, it needs to set the `$LOGNAME` environment variable. Just like OpenSSH `sshd`, it sets this to the same value as `$USER`. Updates tailscale/corp#50380 Signed-off-by: Percy Wegmann <percy@tailscale.com> (cherry picked from commit b1582de)
Close the specific forwarded listener on connection cancellation and stop its callback when the accept loop exits. Canceling a forward no longer retains a connection-lifetime waiter or looks up a replacement listener by address. Test repeated cancel and rebind operations on one connection, along with callback cleanup when the listener closes independently. Make the existing connection-cancellation test fail if its listener stays open. Updates tailscale/corp#50380 Change-Id: Ife7fb0acde012b0abf1a8d804b9c6a8994ee5703 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 055c809)
…ailscale#21980) connect read a non-101 response body with an unbounded io.ReadAll and inlined it into the returned error. * Cap the read, format resp.Status instead of the always-nil err, and use %q. * Add a regression test for the bound Updates: tailscale/corp#50441 Change-Id: Ie35fada982ccda4328fd4e9adb795947dee3a283 Signed-off-by: Pawel Rutkowski <pawel@tailscale.com> (cherry picked from commit 73b4c49)
…21966) tailscale serve --tcp=<port> unix:<path> dialed the target socket with no check, so pointing it at tailscaled's own LocalAPI socket proxied a tailnet peer straight into the LocalAPI. The LocalAPI authorizes a unix-socket caller by the peer credentials of the dialing process -- tailscaled itself, as root -- so the peer got a write-capable LocalAPI and could change the node's settings. The HTTP proxy path already refused this via isTailscaledSocket; the TCP-forward path, added later, did not. Reject it at config-set time in setServeConfigLocked so "tailscale serve" fails up front, and keep a dial-site guard returning ErrProxyToTailscaledSocket as a backstop for configs set by other means. RELNOTE: tailscale serve no longer proxies to tailscaled's own socket Fixes tailscale/corp#50425 Change-Id: I5ed02e83d32bdfb09c8164907bf38cabcc406169 Signed-off-by: Sam Jean <samj@tailscale.com> (cherry picked from commit 713be26)
Since the incubator parses the UID in base 0 via flags.IntVar() and isRootUser() uses base 10, it was possible to get inconsistent results between the two using non-base 10 numbers. Updates tailscale/corp#50380 Change-Id: I9b2fb16bc37153df7b695533cc73a3499cb774eb Signed-off-by: Francois Marier <francois@tailscale.com> (cherry picked from commit e8ebe6a)
The unix-socket reverse proxy checked its backend against the tailscaled socket only when the handler was built, but each backend connection re-resolved the path from scratch. A backend that was benign at config time could later be replaced with a symlink to the tailscaled socket by anyone able to write to its directory, and the next dial followed it. The proxy dials as root, so the same swap could redirect a serve or funnel backend at any root-only unix socket on the host. Move the check to the dial itself, in a dialServeUnixSocket method shared by the HTTP transport, the h2c transport and the TCP-forward handler, all of which previously dialed unix on their own. It refuses tailscaled's socket with ErrProxyToTailscaledSocket immediately before connecting. On Linux 5.6 and newer, the dial pins the resolved socket inode with openat2(2): an O_PATH descriptor opened with RESOLVE_NO_MAGICLINKS, and RESOLVE_BENEATH under the socket's parent directory where the path allows it, or by whole path otherwise. The pinned file is checked against the tailscaled socket, and the connection is made to the pinned file through /proc/self/fd, so replacing the path cannot retarget the connect. On kernels without openat2(2), and on other platforms, the path is dialed directly. The connected peer is re-checked after the dial on every platform. The peer address of a path-bound listener is the path the socket was bound to, not the path that was dialed, so a symlink swapped in before the dial is identified after the connect. This check is best-effort: connections that have no determinable peer name are allowed. Fixes tailscale/corp#50481 Change-Id: Iae5db207519db9c139a512c0427dce3dfa3ca672 Co-authored-by: James Tucker <james@tailscale.com> Signed-off-by: Sam Jean <samj@tailscale.com> Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit 3a38840)
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 tailscale#21885 Signed-off-by: James Tucker <james@tailscale.com> (cherry picked from commit ae4ab7e)
Signed-off-by: Nick O'Neill <nick@tailscale.com>
Release 1.104.4
MYanello
approved these changes
Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #36
This takes this repo from v1.98.5 -> v1.104.4