Skip to content

fix: ship the embedded tunnel in Linux builds only (closes #890) - #896

Merged
ako merged 2 commits into
mainfrom
fix/890-chisel-linux-only
Aug 14, 2026
Merged

fix: ship the embedded tunnel in Linux builds only (closes #890)#896
ako merged 2 commits into
mainfrom
fix/890-chisel-linux-only

Conversation

@ako

@ako ako commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #890.

Restricts the embedded tunnel (chisel) to Linux builds, so the Windows and macOS release binaries no longer contain a dual-use tunnelling tool they can never use.

Why

Chisel tunnels SSH over WebSocket and appears in threat intelligence as a post-exploitation pivoting component. Every platform linked it even though the tunnel only ever runs inside a Linux container, so Defender flags the Windows binary as Trojan:Script/Sabsik.EN.A!ml and enterprise EDR flags this class of payload harder still — blocking mxcli on the managed corporate endpoints most Mendix developers work on.

Not the same problem as #185 (Wacatac.C!ml), which was a genuine generic Go-binary false positive. Here the capability really was in the binary, so code signing would not have addressed it — a signed binary containing chisel is still flagged behaviourally. Nothing was obfuscated, packed or renamed to evade detection; the fix is to stop shipping the capability where it is unused. Rationale is recorded in ADR-0009.

The seam

Two chisel imports, one interface each, _linux.go implementation + !linux stub:

Seam Package Linux impl Stub
tunnelConn / startTunnel cmd/mxcli/docker tunnel_linux.go tunnel_other.go
controlServer / newControlServer cmd/mxcli/tunnelhub control_linux.go control_other.go

The hub seam is at Start, not construction, on purpose — a Server that cannot bind a control server is still a Server whose routing can be exercised, so the portable front (registry, API, auth, Host routing) stays testable on every platform rather than only where the tunnel ships.

Both commands stay registered and documented everywhere and fail with an actionable message:

$ mxcli run --hub https://hub.example.com -p app.mpr
Error: the browser preview tunnel (--hub) is only available in Linux builds of mxcli

--hub reverse-tunnels the running app out to a tunnel-hub, and that tunnel ships
only in the Linux build, because that is the only place it runs: inside the
container. Windows and macOS builds leave it out deliberately.

Run mxcli inside the project's devcontainer (or any Linux container) to use --hub.
Everything else about `mxcli run --local` works here unchanged.

See https://mendixlabs.github.io/mxcli/tools/run-local.html

Failure is raised during flag validation, before booting an app or touching cert caches / key stores.

Proof

Dependency graph — 32 packages leave windows/darwin, including the whole x/crypto/ssh stack, gorilla/websocket, armon/go-socks5, x/net/proxy and the jpillora/* support libraries:

GOOS=windows go list -deps ./... | grep -i chisel   ->  (nothing)
GOOS=darwin  go list -deps ./... | grep -i chisel   ->  (nothing)
GOOS=linux   go list -deps ./... | grep -i chisel   ->  9 packages

Binary inspection (windows/amd64, release flags): go version -m lists no chisel/websocket/socks5/ssh module; strings finds 0 chisel literals (was 432). Linux still carries the module and 450 literals.

⚠️ go tool nm cannot prove this and was deliberately not used as the criterion: release ldflags are -s -w, which strips the symbol table, so nm reports no symbols whether or not chisel is linked. Verified — an unstripped Windows build has 405 chisel symbols while the stripped release-equivalent had none before this change. Used alone it is a false pass.

Size (release flags, amd64, embed dir held identical on both sides):

before after delta
windows 91,808,768 78,336,000 -13,472,768 (-14.67%)
darwin 91,878,912 78,401,200 -13,477,712 (-14.67%)
linux 90,161,314 90,169,506 +8,192 (+0.01%)

(cmd/mxcli/skills/ is gitignored and regenerated by make sync-skills; a naive before/after straddling a sync misattributes ~240 KB of doc churn to the code change.)

Linux still worksTestTunnelRoundTrip (real chisel client + server, end to end) and TestFront_ProxiesThroughTunnel (hub front proxying through a live tunnel) both pass.

Regression guard

scripts/check-tunnel-deps.sh, wired into push-test.yml and make check-tunnel-deps:

  • Matches the module list, not the string chisel, so transitive re-entry via x/crypto/ssh also trips it.
  • Asserts a positive control first (chisel is in the linux graph) so a broken go list or a typo'd pattern cannot make every platform look clean.
  • Proven to fail: re-adding the import made it fail on all four windows/darwin targets, listing 15 packages.

A new CI matrix job (tunnel-seam-cross-platform, windows-latest + macos-latest) executes the !linux tests for real — neither package needs the generated ANTLR parser, so it is fast.

Breaking changes (narrow)

  • mxcli tunnel-hub can no longer be hosted on Windows or macOS. Move the hub to a Linux host. Judged near-zero impact (it is a public-facing daemon) but it is a genuine capability removal, not just repackaging.
  • Native Windows/macOS installs cannot use --hub — run mxcli inside the devcontainer, which is where the warm loop already runs.
  • tunnelhub.ServerOptions.ChiselAddrControlAddr (it now addresses a platform-agnostic control server). This rename also removed the last four chisel-derived string literals from the non-Linux binaries; that is naming accuracy, not concealment — control_linux.go, tunnel_linux.go and go.mod name chisel plainly, and it is only acceptable because the code is genuinely gone. Called out explicitly in ADR-0009.

Validation

  • make build, make test (full suite), make lint (Go + TypeScript) — all pass.
  • go build + go vet ./... clean for linux, windows and darwin (vet type-checks every test file).
  • The !linux unit tests compile for both targets here but were not executed locally — this container has no Windows/macOS runner. That is exactly the gap the new CI matrix job closes; it will be the first real execution.
  • Mendix Studio Pro validation: N/A. This change touches no MDL, no BSON serialization, and no project behaviour — it is a build-configuration and CLI-surface change only. No .mpr is read or written differently.

Docs

ADR-0009 (rationale + rejected alternatives, incl. why signing and shell-out don't work), a load-bearing rule in CLAUDE.md, a symptom row in .claude/skills/fix-issue.md, CHANGELOG, README, docs-site/src/tools/run-local.md, docs-site/src/tutorial/claude-code-web.md, and the two synced mendix/ skills.

🤖 Generated with Claude Code

mxcli embeds chisel to reverse-tunnel a locally-running app out to a
tunnel-hub preview URL. Chisel is a dual-use tool that appears in threat
intelligence as a post-exploitation pivoting component, and every platform
linked it even though the tunnel only ever runs inside a Linux container.
Windows Defender flags the Windows binary as Trojan:Script/Sabsik.EN.A!ml
and enterprise EDR flags this class of payload harder still, which blocks
mxcli on the managed corporate endpoints most Mendix developers use.

Both chisel imports now sit behind a one-interface, Linux-only seam:

  tunnelConn   / startTunnel      cmd/mxcli/docker      (chisel client)
  controlServer/ newControlServer cmd/mxcli/tunnelhub   (chisel server)

each with a _linux.go implementation and a !linux stub. The hub seam is at
Start rather than construction so the portable front - registry, API, auth,
Host routing - stays testable on every platform. Both commands remain
registered and documented everywhere and fail with an actionable message
naming the Linux-container constraint and a docs link.

Windows and macOS binaries lose 32 packages, including the whole
x/crypto/ssh stack, gorilla/websocket and armon/go-socks5: -13.5 MB
(-14.67%). Linux is unchanged in behaviour and grows 8 KB.

scripts/check-tunnel-deps.sh (CI + make check-tunnel-deps) fails the build
if chisel or its tunnelling-specific dependencies reappear in a
windows/darwin dependency graph. It matches the module list rather than the
string "chisel", so transitive re-entry via x/crypto/ssh also trips it, and
it asserts a positive control first so it cannot pass vacuously. Verified
by re-adding the import and watching it fail on all four targets. A new CI
matrix job runs the !linux tests on real Windows and macOS runners.

Note this is a different problem from #185 (Wacatac.C!ml), which was a
genuine generic Go-binary false positive. Here the capability really was in
the binary, so code signing would not address it. Nothing was obfuscated,
packed or renamed to evade detection - the fix is to stop shipping the
capability where it is unused. Rationale recorded in ADR-0009.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new windows/macOS job ran the full docker + tunnelhub packages, which
fails on Windows for reasons that predate this branch: three tests assert
POSIX file modes (0600) that Windows does not implement (os.Chmod only
toggles the read-only bit, so Stat reports 666), one asserts a
world-readable file is refused, and one assumes Unix path separators.
macOS passes all of them. Filed as #897.

Scope the job with -run to the seam's own !linux tests. The full test
binaries are still compiled for each platform, so a Windows/macOS build
break is still caught; only the stub behaviour is executed. Assert that at
least 4 seam tests actually ran, since -run passes vacuously when its
pattern matches nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ako
ako merged commit 76ab0dd into main Aug 14, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restrict the embedded chisel tunnel to Linux builds (Windows/macOS binaries flagged by Defender + EDR)

1 participant