Cross-compile Tailscale static binaries (tailscale + tailscaled) for macOS 10.14 Mojave (darwin/amd64) on a Linux host.
Tailscale officially dropped macOS 10.14 support. This project builds a compatible binary by using Go's -overlay mechanism to patch the standard library, replacing macOS 12+ APIs with Mojave-compatible equivalents.
Go 1.26's crypto/x509 standard library references SecTrustCopyCertificateChain, a Security framework API available only on macOS 12+. Even with CGO_ENABLED=0, Go on darwin dynamically loads Security framework symbols at runtime via //go:cgo_import_dynamic, causing an immediate crash on Mojave:
dyld: Symbol not found: _SecTrustCopyCertificateChain
Referenced from: ./tailscale (which was built for Mac OS X 12.0)
Abort trap: 6
| Approach | Why It Fails |
|---|---|
CGO_ENABLED=0 |
Go on darwin still loads system frameworks via //go:cgo_import_dynamic + assembly trampoline — not truly zero system dependency |
MACOSX_DEPLOYMENT_TARGET=10.14 |
Only changes Mach-O header minimum version tag, does NOT change which APIs the Go stdlib references |
Use Go's -overlay build flag to replace 3 standard library files at compile time, swapping the macOS 12+ API for two older Mojave-compatible APIs:
| New API (macOS 12+) | Old API Replacement (macOS 10.7+) |
|---|---|
SecTrustCopyCertificateChain(trust) — returns entire cert chain as CFArray |
SecTrustGetCertificateCount(trust) — get chain length |
SecTrustGetCertificateAtIndex(trust, i) — get cert one by one |
| File | Change |
|---|---|
crypto/x509/internal/macos/security.go |
Remove SecTrustCopyCertificateChain, add SecTrustGetCertificateCount + SecTrustGetCertificateAtIndex with cgo_import_dynamic |
crypto/x509/internal/macos/security.s |
Remove old assembly trampoline, add two new trampolines |
crypto/x509/root_darwin.go |
Change from single CFArray fetch to count + index loop |
Before (macOS 12+):
chainRef, err := macos.SecTrustCopyCertificateChain(trustObj)
defer macos.CFRelease(chainRef)
for i := 0; i < macos.CFArrayGetCount(chainRef); i++ {
certRef := macos.CFArrayGetValueAtIndex(chainRef, i)
// ...
}After (macOS 10.7+ compatible):
certCount := macos.SecTrustGetCertificateCount(trustObj)
for i := 0; i < certCount; i++ {
certRef := macos.SecTrustGetCertificateAtIndex(trustObj, i)
// ...
}Note:
SecTrustGetCertificateAtIndexuses "Get" semantics (borrowed ref) — noCFReleaseneeded.SecTrustCopyCertificateChainuses "Copy" semantics (caller owns) — requiresCFRelease. This follows Apple's Core Foundation memory management rules.
- Go 1.23+ (tested with 1.23 and 1.26)
- git
- Python 3 (for verification script)
- Linux build host (cross-compile)
Go 1.26 renamed the internal package crypto/x509/internal/macOS to macos (lowercase). This project provides two sets of overlay files that are automatically selected based on your Go version:
| Go Version | Overlay | Package Name | Directory |
|---|---|---|---|
| 1.23 – 1.25 | go123 |
macOS (capital S) |
overlay/go123/ |
| 1.26+ | go126 |
macos (lowercase) |
overlay/go126/ |
The Makefile and CI auto-detect the Go version. To override manually:
make GO_OVERLAY=go123 # Force Go 1.23-1.25 overlay
make GO_OVERLAY=go126 # Force Go 1.26+ overlayCheck detected version:
make check-gomakeOr use the build script directly:
./build_tailscale-1.76.3_macOS_mojave.shOutput binaries are written to mojave_amd64/.
make verifyValidates built binaries against mach-o-header-symbols.json:
- Mach-O header (magic, cputype, cpusubtype, filetype, flags)
- Required symbols present (
SecTrustGetCertificateCount,SecTrustGetCertificateAtIndex) - Forbidden symbols absent (
SecTrustCopyCertificateChain)
scp mojave_amd64/{tailscale,tailscaled,run_tailscale.sh} user@mojave-host:~/On the Mojave machine:
sudo ./run_tailscale.shAfter deployment, tailscale status may hang because the CLI defaults to the macOS GUI socket path. The self-compiled tailscaled daemon uses /var/run/tailscaled.sock. Fix with an alias:
# Add to ~/.bashrc
alias tailscale="/path/to/tailscale --socket=/var/run/tailscaled.sock"build_tailscale-1.76.3_macOS_mojave.sh # Main build script
Makefile # Build/deploy/verify targets
overlay/ # Go stdlib patches (tracked in git)
crypto/x509/internal/macos/
security.go # Patched: old API replacements
security.s # Patched: new assembly trampolines
crypto/x509/
root_darwin.go # Patched: count+index loop
run_tailscale.sh # Startup helper for Mojave
overlay/ # Go stdlib patches
go123/ # For Go 1.23-1.25 (package macOS)
go126/ # For Go 1.26+ (package macos)
scripts/verify_macho.py # Mach-O header + symbol verification
mach-o-header-symbols.json # Reference spec for Mojave compatibility
.github/workflows/build.yml # CI: build + verify + release on tag
mojave_amd64/ # Build output (git-ignored)
src/ # Cloned Tailscale source (git-ignored)
Note:
overlay.jsonis generated at build time with correct absolute paths. It is not tracked in git to avoid leaking local filesystem paths.
CGO_ENABLED=0on darwin ≠ zero system dependency — Go loads Security.framework at runtime via//go:cgo_import_dynamic+ assembly trampolineMACOSX_DEPLOYMENT_TARGETcannot fix symbol issues — it only changes the Mach-O header, not which APIs the code references- Go
-overlayis the correct way to patch stdlib — no need to fork Go or modify$GOROOT - Apple Core Foundation memory rules matter —
Copy/Create= caller mustCFRelease;Get= borrowed, no release needed
Pre-commit hooks are configured. To set up:
python3 -m venv venv
source venv/bin/activate
pip install pre-commit
pre-commit install