From c695e579fa08a642f2cff062832076a540fe7134 Mon Sep 17 00:00:00 2001 From: Codinget Date: Tue, 28 Jul 2026 17:44:01 +0000 Subject: [PATCH] fix(tsconnect): link fork features into the wasm build 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 --- cmd/tsconnect/wasm/wasm_js.go | 1 + cmd/tsconnect/wasmbuild/wasmbuild.go | 70 +++++++++++----------------- 2 files changed, 28 insertions(+), 43 deletions(-) diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index 480b2c6b5..9fb2af200 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -26,6 +26,7 @@ import ( "golang.org/x/crypto/ssh" "tailscale.com/control/controlclient" + _ "tailscale.com/feature/condregister" "tailscale.com/ipn" "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnlocal" diff --git a/cmd/tsconnect/wasmbuild/wasmbuild.go b/cmd/tsconnect/wasmbuild/wasmbuild.go index bb2814027..ceefd86fe 100644 --- a/cmd/tsconnect/wasmbuild/wasmbuild.go +++ b/cmd/tsconnect/wasmbuild/wasmbuild.go @@ -36,49 +36,36 @@ var baseTags = []string{ "omitpemdecrypt", } -// Keep is the set of feature/featuretags tags the cmd/tsconnect/wasm -// build needs LINKED. Every other feature in [featuretags.Features] is -// excluded via its ts_omit_ build tag (computed by [Tags]). -// Transitive dependencies of entries in Keep are pulled in -// automatically via [featuretags.Requires]. +// 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. // -// Adding an entry here grows the wasm bundle. Removing one strips it. // The init() below panics if any entry is unknown to feature/featuretags, // so a rename / removal in that registry fails loudly here. // -// Notably absent (server-only or otherwise meaningless in a browser): -// - "ssh": controls the SSH *server* (feature/ssh registers -// ssh/tailssh). The wasm acts as an SSH *client* using -// golang.org/x/crypto/ssh directly; no featuretag gates that. -// - "portmapper", "debugportmapper": js/wasm has no UDP sockets, -// can't speak NAT-PMP / PCP / UPnP. -// - "captiveportal": the browser handles captive portal detection -// in front of us. -// - "syspolicy": no MDM in a browser. -// - "drive", "taildrop", "peerapi*": no local filesystem. -// - "clientupdate": no binary self-update. -// - "dbus", "resolved", "networkmanager", "iptables", "linkspeed", -// "linuxdnsfight", "listenrawdisco", "osrouter", "synology", -// "systray", "tundevstats", "wakeonlan": OS integrations not -// applicable to a browser-hosted client. -// - "aws", "cloud", "kube", "bird", "appconnectors", "conn25", -// "relayserver", "serve", "acme", "tap", "tpm", "doctor", -// "advertiseroutes", "advertiseexitnode", "useroutes", -// "useexitnode": server-side or otherwise out of scope for the -// SSH-in-browser / fetch-in-browser use case. -var Keep = []featuretags.FeatureTag{ - "c2n", // control-to-node mechanism the control client invokes - "dns", // MagicDNS resolution in-process - "health", // ipnstate/ipnlocal reference health warnables pervasively - "ipnbus", // notification bus for state/netmap callbacks - "logtail", // log upload (browser console + remote) - "netstack", // userspace networking; wasm has no kernel TUN +// 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 Keep { + for _, ft := range Omit { if _, ok := featuretags.Features[ft]; !ok { - panic(fmt.Sprintf("wasmbuild.Keep references unknown feature tag %q; "+ + panic(fmt.Sprintf("wasmbuild.Omit references unknown feature tag %q; "+ "did feature/featuretags rename or remove it?", ft)) } } @@ -103,25 +90,22 @@ type BuildInfo struct { } // Tags returns the joined -tags value for the wasm build: [baseTags] -// plus a ts_omit_ for every entry in [featuretags.Features] -// that is not transitively required by [Keep]. +// plus a ts_omit_ 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 { - keep := map[featuretags.FeatureTag]bool{} - for _, ft := range Keep { - for dep := range featuretags.Requires(ft) { - keep[dep] = true - } + 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 !keep[ft] { + if omit[ft] { tags = append(tags, ft.OmitTag()) } }