Files
tailscale/flake.nix
Brad Fitzpatrick 04ae61fe4b tstest/integration/jswasmtest: add headless-Chromium tests for @tailscale/connect
Add Go tests that drive a real headless Chromium (via chromedp) against
the built cmd/tsconnect/pkg/ artifact and verify the @tailscale/connect
public API surface end-to-end. The package has not been republished in
three years, in part because no test exercises the produced artifact at
runtime — only tsc --noEmit and a Go build run in CI.

TestCreateIPN loads pkg.js into the browser, calls createIPN with a junk
auth key, and asserts that pkg.createIPN / pkg.runSSHSession are
functions and that createIPN() returns an IPN with the documented
run/login/logout/ssh/fetch methods. No control-plane traffic.

TestFetchTailnetPeer stands up a full local tailnet (testcontrol +
DERP + a tsnet.Server peer) and verifies that the browser-side WASM
client can join over WebSocket-noise to the same control, connect to
DERP over WSS, and then ipn.fetch() an HTTP service hosted on the tsnet
peer through the tailnet. The test asserts the response body matches a
known string. Browser state transitions are logged: NoState -> NeedsLogin
-> Starting -> Running.

Tests are opt-in via --run-headless-browser-tests (matching the existing
--run-vm-tests pattern in tstest/natlab/vmtest) so they never fire in
casual `go test ./...` runs. When the flag is set, a test is skipped if
cmd/tsconnect/pkg/ has not been built, and fails with t.Error if no
chromium binary is found on $PATH (honoring $CHROME_BIN as an override).
findChromium also falls back to /Applications/Google Chrome.app and
/Applications/Chromium.app on darwin, since macOS Chrome's executable
lives inside an .app bundle and is not on $PATH by default. The
.github/workflows/test.yml wasm job is extended to install
google-chrome-stable and run the tests with the flag after build-pkg.

To prevent silently testing a stale pkg/main.wasm (built from an older
checkout than the rest of the test invocation), build-pkg now writes
pkg/build-info.json recording the sha256 of the raw (pre-wasm-opt)
go-build output. The test does its own `go build` of
cmd/tsconnect/wasm with the same -tags/-trimpath/-ldflags (factored
into a new cmd/tsconnect/wasmbuild package shared by both call sites)
and t.Fatalfs with a "rebuild" instruction on mismatch. Cost is
near-zero because the Go build cache from the prior build-pkg makes
the rebuild a cache hit.

The new wasmbuild package also replaces cmd/tsconnect's hardcoded -tags
string with a minimal-feature-set computation. wasmbuild.Keep names the
small set of feature/featuretags entries the browser client actually
needs (netstack, logtail, dns, health, c2n, ipnbus); wasmbuild.Tags()
emits a ts_omit_<f> for every other
omittable feature in feature/featuretags.Features, with transitive deps
expanded via featuretags.Requires. An init() panics if Keep references
a feature unknown to feature/featuretags so a rename there fails
loudly. Net effect on size: 32M raw / 9.4M brotli before this change,
25M raw / 4.4M brotli after — vs the last-published 1.39.98 at 21M /
3.8M. The transitive package-import graph is unchanged (176
tailscale.com/* packages either way): featuretags omits eliminate
dead code via `const HasX = false`, not imports. Trimming the import
graph would require a separate, larger refactor splitting interface
packages by build tag.

Writing TestFetchTailnetPeer surfaced several real issues, all fixed
here:

  * cmd/tsconnect built the wasm with the nethttpomithttp2 tag, but
    control/ts2021 (since commit 1d93bdce2, "control/controlclient:
    remove x/net/http2, use net/http", Oct 2025) requires HTTP/2 from
    net/http's bundled implementation. With nethttpomithttp2 set, the
    bundle is excluded and the wasm client cannot speak HTTP/2 to any
    control plane, including production. Drop the tag. Wasm size grows
    ~1 MB raw / ~300 KB brotli (more than offset by the feature
    pruning above). The last published @tailscale/connect (1.39.98,
    early 2023) pre-dates the regression, which is why no consumer has
    reported the breakage.

  * tstest/integration/testcontrol.Server's /ts2021 noise upgrade
    endpoint rejected anything but POST. WebSocket clients (the only
    transport available to browser-WASM) come in as GET. Allow both;
    the controlhttp AcceptHTTP path dispatches on the Upgrade header,
    so the websocket library still enforces GET for WS upgrades.
    This matches production, where the same controlhttpserver.AcceptHTTP
    routes purely on the Upgrade header without checking method.

  * derp/derphttp's urlString built the DERP URL from node.HostName
    only, dropping node.DERPPort. Non-WS clients use a separate code
    path (connectToHost) that honors DERPPort, but WebSocket-only
    clients (browser-WASM) went through urlString and so could not
    reach a DERP running on any port other than 443. Include the port
    when it differs from the scheme default.

Also move addWebSocketSupport from cmd/derper (where it was main-only)
to derp/derpserver.AddWebSocketSupport so tstest/integration.RunDERPAndSTUN
can wrap its DERP handler with WebSocket support — without that, the
test DERP would not accept the browser's wss connection.

Fixes #9394

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Iff9cdee303e3b239924249b5bffb2fd04e02f391
2026-05-20 10:48:29 -07:00

168 lines
6.4 KiB
Nix

# flake.nix describes a Nix source repository that provides
# development builds of Tailscale and the fork of the Go compiler
# toolchain that Tailscale maintains. It also provides a development
# environment for working on tailscale, for use with "nix develop".
#
# For more information about this and why this file is useful, see:
# https://wiki.nixos.org/wiki/Flakes
#
# Also look into direnv: https://direnv.net/, this can make it so that you can
# automatically get your environment set up when you change folders into the
# project.
#
# WARNING: currently, the packages provided by this flake are brittle,
# and importing this flake into your own Nix configs is likely to
# leave you with broken builds periodically.
#
# The issue is that building Tailscale binaries uses the buildGoModule
# helper from nixpkgs. This helper demands to know the content hash of
# all of the Go dependencies of this repo, in the form of a Nix SRI
# hash. This hash isn't automatically kept in sync with changes made
# to go.mod yet, and so every time we update go.mod while hacking on
# Tailscale, this flake ends up with a broken build due to hash
# mismatches.
#
# Right now, this flake is intended for use by Tailscale developers,
# who are aware of this mismatch and willing to live with it. At some
# point, we'll add automation to keep the hashes more in sync, at
# which point this caveat should go away.
#
# See https://github.com/tailscale/tailscale/issues/6845 for tracking
# how to fix this mismatch.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
systems.url = "github:nix-systems/default";
# Used by shell.nix as a compat shim.
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs = {
self,
nixpkgs,
systems,
flake-compat,
}: let
goVersion = nixpkgs.lib.fileContents ./go.toolchain.version;
toolChainRev = nixpkgs.lib.fileContents ./go.toolchain.rev;
flakeHashes = builtins.fromJSON (builtins.readFile ./flakehashes.json);
gitHash = flakeHashes.toolchain.sri;
eachSystem = f:
nixpkgs.lib.genAttrs (import systems) (system:
f (import nixpkgs {
system = system;
overlays = [
(final: prev: {
go_1_26 = prev.go_1_26.overrideAttrs (old: {
version = goVersion;
src = prev.fetchFromGitHub {
owner = "tailscale";
repo = "go";
rev = toolChainRev;
sha256 = gitHash;
};
# The Tailscale Go fork carries a placeholder in
# src/runtime/debug/mod.go that must be replaced with
# the actual toolchain git rev at build time. Without
# this, binaries report an empty tailscale.toolchain.rev
# and the runtime assertion in
# assert_ts_toolchain_match.go panics.
postPatch =
(old.postPatch or "")
+ ''
substituteInPlace src/runtime/debug/mod.go \
--replace-fail "TAILSCALE_GIT_REV_TO_BE_REPLACED_AT_BUILD_TIME" "${toolChainRev}"
'';
});
})
];
}));
tailscaleRev = self.rev or "";
in {
# tailscale takes a nixpkgs package set, and builds Tailscale from
# the same commit as this flake. IOW, it provides "tailscale built
# from HEAD", where HEAD is "whatever commit you imported the
# flake at".
#
# This is currently unfortunately brittle, because we have to
# specify vendorHash, and that sha changes any time we alter
# go.mod. We don't want to force a nix dependency on everyone
# hacking on Tailscale, so this flake is likely to have broken
# builds periodically until someone comes through and manually
# fixes them up. I sure wish there was a way to express "please
# just trust the local go.mod, vendorHash has no benefit here",
# but alas.
#
# So really, this flake is for tailscale devs to dogfood with, if
# you're an end user you should be prepared for this flake to not
# build periodically.
packages = eachSystem (pkgs: rec {
default = pkgs.buildGo126Module {
name = "tailscale";
pname = "tailscale";
src = ./.;
vendorHash = flakeHashes.vendor.sri;
nativeBuildInputs = [pkgs.makeWrapper pkgs.installShellFiles];
ldflags = ["-X tailscale.com/version.gitCommitStamp=${tailscaleRev}"];
env.CGO_ENABLED = 0;
subPackages = [
"cmd/tailscale"
"cmd/tailscaled"
"cmd/tsidp"
];
doCheck = false;
# NOTE: We strip the ${PORT} and $FLAGS because they are unset in the
# environment and cause issues (specifically the unset PORT). At some
# point, there should be a NixOS module that allows configuration of these
# things, but for now, we hardcode the default of port 41641 (taken from
# ./cmd/tailscaled/tailscaled.defaults).
postInstall =
pkgs.lib.optionalString pkgs.stdenv.isLinux ''
wrapProgram $out/bin/tailscaled --prefix PATH : ${pkgs.lib.makeBinPath [pkgs.iproute2 pkgs.iptables pkgs.getent pkgs.shadow]}
wrapProgram $out/bin/tailscale --suffix PATH : ${pkgs.lib.makeBinPath [pkgs.procps]}
sed -i \
-e "s#/usr/sbin#$out/bin#" \
-e "/^EnvironmentFile/d" \
-e 's/''${PORT}/41641/' \
-e 's/$FLAGS//' \
./cmd/tailscaled/tailscaled.service
install -D -m0444 -t $out/lib/systemd/system ./cmd/tailscaled/tailscaled.service
''
+ pkgs.lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) ''
installShellCompletion --cmd tailscale \
--bash <($out/bin/tailscale completion bash) \
--fish <($out/bin/tailscale completion fish) \
--zsh <($out/bin/tailscale completion zsh)
'';
};
tailscale = default;
});
devShells = eachSystem (pkgs: {
default = pkgs.mkShell {
packages = with pkgs; [
curl
git
gopls
gotools
graphviz
perl
go_1_26
yarn
# qemu and e2fsprogs are needed for natlab
qemu
e2fsprogs
];
};
});
};
}
# nix-direnv cache busting line: sha256-HCYBBM2rp4wuwS6x4fvbpJ2R9WHoT5tC1t7d6jtj/n8=