Upstream's featuretags work turned the wasm build into an allow-list scoped to its SSH-in-browser client, which strips most of what this fork's JS bridge exposes. Two problems, both silent at build time: - cmd/tsconnect/wasm never imported feature/condregister, so extensions only registered if the wasm happened to import them directly (taildrop did, ACME did not). Without it getCert/listenTLS/setFunnel fail with "cert support not compiled in this build". - The Keep allow-list omitted acme, serve, taildrop, drive, tailnetlock, bakedroots and the exit node features. bakedroots matters especially: a browser has no system roots, so net/tlsdial's LetsEncrypt fallback is the only verification path there. Invert the polarity to an explicit Omit list, matching how this build behaved before featuretags existed. Only feature/ace is omitted, because it does not compile for GOOS=js. Trimming the bundle is worth doing later with measurements; an allow-list turns each mistake into a runtime failure rather than a build error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package wasmbuild contains the shared build flags and manifest layout
|
|
// used to produce the @tailscale/connect NPM package's main.wasm. It is
|
|
// imported both by cmd/tsconnect (which does the build) and by tests
|
|
// that verify the produced pkg/main.wasm matches what the current
|
|
// source tree would build (see tstest/integration/jswasmtest).
|
|
package wasmbuild
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
|
|
"tailscale.com/feature/featuretags"
|
|
)
|
|
|
|
// baseTags are the non-featuretag build tags always set for the wasm
|
|
// build. Featuretag omits (ts_omit_*) are computed dynamically by
|
|
// [Tags] from [Keep] using the [featuretags] registry.
|
|
//
|
|
// Note: nethttpomithttp2 is intentionally NOT included: control/ts2021
|
|
// (since commit 1d93bdce2, Oct 2025) requires HTTP/2 from net/http's
|
|
// bundled implementation. Excluding it leaves the wasm client unable
|
|
// to negotiate with any control plane.
|
|
var baseTags = []string{
|
|
"tailscale_go",
|
|
"osusergo",
|
|
"netgo",
|
|
"omitidna",
|
|
"omitpemdecrypt",
|
|
}
|
|
|
|
// Omit is the set of feature/featuretags tags excluded from the
|
|
// cmd/tsconnect/wasm build via their ts_omit_ build tag (computed by
|
|
// [Tags]). Everything else in [featuretags.Features] stays linked.
|
|
//
|
|
// Upstream uses the opposite polarity here — a small allow-list — because
|
|
// its wasm client is only an SSH/fetch-in-browser tool. This fork's JS
|
|
// bridge exposes Taildrop, Taildrive, Funnel/serve, ACME certs, exit node
|
|
// selection, service advertisement and the peerAPI, so an allow-list is
|
|
// the wrong default: a missing entry is not a compile error, it is a
|
|
// feature that silently stops working at runtime (an omitted extension
|
|
// simply never registers its hooks). Linking everything also matches how
|
|
// this build behaved before upstream introduced featuretags.
|
|
//
|
|
// The init() below panics if any entry is unknown to feature/featuretags,
|
|
// so a rename / removal in that registry fails loudly here.
|
|
//
|
|
// Trimming the bundle by omitting more features is worthwhile but should
|
|
// be done with measurements and per-feature runtime verification, not by
|
|
// assuming a feature is unreachable from the browser.
|
|
var Omit = []featuretags.FeatureTag{
|
|
// feature/ace does not compile for GOOS=js: control/controlhttp only
|
|
// installs HookMakeACEDialer on non-js platforms, so feature/ace's
|
|
// reference to it is undefined here.
|
|
"ace",
|
|
}
|
|
|
|
func init() {
|
|
for _, ft := range Omit {
|
|
if _, ok := featuretags.Features[ft]; !ok {
|
|
panic(fmt.Sprintf("wasmbuild.Omit references unknown feature tag %q; "+
|
|
"did feature/featuretags rename or remove it?", ft))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ProdLDFlags is the -ldflags value used in production wasm builds.
|
|
// -s strips the symbol table, -w strips DWARF, both to shrink the
|
|
// shipped artifact.
|
|
const ProdLDFlags = "-s -w"
|
|
|
|
// BuildInfoFile is the basename of the JSON manifest that build-pkg
|
|
// writes alongside main.wasm, recording the sha256 of the raw
|
|
// (pre-wasm-opt) go-build output. Tests use this to detect a stale
|
|
// pkg/main.wasm without having to re-run wasm-opt themselves.
|
|
const BuildInfoFile = "build-info.json"
|
|
|
|
// BuildInfo is the JSON contents of [BuildInfoFile].
|
|
type BuildInfo struct {
|
|
// RawWasmSHA256 is the lowercase hex sha256 of the wasm bytes as
|
|
// they came out of `go build` (before wasm-opt was run in place).
|
|
RawWasmSHA256 string `json:"raw_wasm_sha256"`
|
|
}
|
|
|
|
// Tags returns the joined -tags value for the wasm build: [baseTags]
|
|
// plus a ts_omit_<feature> for every entry in [Omit].
|
|
//
|
|
// The result is sorted so that the same source tree always produces
|
|
// the same string (and therefore the same wasm bytes, given identical
|
|
// inputs to `go build`).
|
|
func Tags() string {
|
|
omit := map[featuretags.FeatureTag]bool{}
|
|
for _, ft := range Omit {
|
|
omit[ft] = true
|
|
}
|
|
tags := slices.Clone(baseTags)
|
|
for ft := range featuretags.Features {
|
|
if ft == "" || !ft.IsOmittable() {
|
|
continue
|
|
}
|
|
if omit[ft] {
|
|
tags = append(tags, ft.OmitTag())
|
|
}
|
|
}
|
|
slices.Sort(tags)
|
|
return strings.Join(tags, ",")
|
|
}
|
|
|
|
// ProdCommand returns an *exec.Cmd that runs `go build` for
|
|
// cmd/tsconnect/wasm with the production flags, writing the raw
|
|
// (pre-wasm-opt) wasm to outputPath. GOOS=js GOARCH=wasm is set in the
|
|
// command's environment. The caller is responsible for wiring
|
|
// Stdin/Stdout/Stderr and invoking Run.
|
|
//
|
|
// If goBin is empty, runtime.GOROOT()+"/bin/go" is used so that the
|
|
// build runs under the same toolchain that built the caller.
|
|
func ProdCommand(goBin, outputPath string) *exec.Cmd {
|
|
if goBin == "" {
|
|
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
|
|
}
|
|
cmd := exec.Command(goBin, "build",
|
|
"-tags", Tags(),
|
|
"-trimpath",
|
|
"-ldflags", ProdLDFlags,
|
|
"-o", outputPath,
|
|
"tailscale.com/cmd/tsconnect/wasm",
|
|
)
|
|
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
|
return cmd
|
|
}
|