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 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
|
_ "tailscale.com/feature/condregister"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnauth"
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
|
|||||||
@@ -36,49 +36,36 @@ var baseTags = []string{
|
|||||||
"omitpemdecrypt",
|
"omitpemdecrypt",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep is the set of feature/featuretags tags the cmd/tsconnect/wasm
|
// Omit is the set of feature/featuretags tags excluded from the
|
||||||
// build needs LINKED. Every other feature in [featuretags.Features] is
|
// cmd/tsconnect/wasm build via their ts_omit_ build tag (computed by
|
||||||
// excluded via its ts_omit_ build tag (computed by [Tags]).
|
// [Tags]). Everything else in [featuretags.Features] stays linked.
|
||||||
// Transitive dependencies of entries in Keep are pulled in
|
//
|
||||||
// automatically via [featuretags.Requires].
|
// 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,
|
// The init() below panics if any entry is unknown to feature/featuretags,
|
||||||
// so a rename / removal in that registry fails loudly here.
|
// so a rename / removal in that registry fails loudly here.
|
||||||
//
|
//
|
||||||
// Notably absent (server-only or otherwise meaningless in a browser):
|
// Trimming the bundle by omitting more features is worthwhile but should
|
||||||
// - "ssh": controls the SSH *server* (feature/ssh registers
|
// be done with measurements and per-feature runtime verification, not by
|
||||||
// ssh/tailssh). The wasm acts as an SSH *client* using
|
// assuming a feature is unreachable from the browser.
|
||||||
// golang.org/x/crypto/ssh directly; no featuretag gates that.
|
var Omit = []featuretags.FeatureTag{
|
||||||
// - "portmapper", "debugportmapper": js/wasm has no UDP sockets,
|
// feature/ace does not compile for GOOS=js: control/controlhttp only
|
||||||
// can't speak NAT-PMP / PCP / UPnP.
|
// installs HookMakeACEDialer on non-js platforms, so feature/ace's
|
||||||
// - "captiveportal": the browser handles captive portal detection
|
// reference to it is undefined here.
|
||||||
// in front of us.
|
"ace",
|
||||||
// - "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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
for _, ft := range Keep {
|
for _, ft := range Omit {
|
||||||
if _, ok := featuretags.Features[ft]; !ok {
|
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))
|
"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]
|
// Tags returns the joined -tags value for the wasm build: [baseTags]
|
||||||
// plus a ts_omit_<feature> for every entry in [featuretags.Features]
|
// plus a ts_omit_<feature> for every entry in [Omit].
|
||||||
// that is not transitively required by [Keep].
|
|
||||||
//
|
//
|
||||||
// The result is sorted so that the same source tree always produces
|
// The result is sorted so that the same source tree always produces
|
||||||
// the same string (and therefore the same wasm bytes, given identical
|
// the same string (and therefore the same wasm bytes, given identical
|
||||||
// inputs to `go build`).
|
// inputs to `go build`).
|
||||||
func Tags() string {
|
func Tags() string {
|
||||||
keep := map[featuretags.FeatureTag]bool{}
|
omit := map[featuretags.FeatureTag]bool{}
|
||||||
for _, ft := range Keep {
|
for _, ft := range Omit {
|
||||||
for dep := range featuretags.Requires(ft) {
|
omit[ft] = true
|
||||||
keep[dep] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tags := slices.Clone(baseTags)
|
tags := slices.Clone(baseTags)
|
||||||
for ft := range featuretags.Features {
|
for ft := range featuretags.Features {
|
||||||
if ft == "" || !ft.IsOmittable() {
|
if ft == "" || !ft.IsOmittable() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !keep[ft] {
|
if omit[ft] {
|
||||||
tags = append(tags, ft.OmitTag())
|
tags = append(tags, ft.OmitTag())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user