Skip to content

Repository files navigation

Tailscale v1.76.3 — macOS 10.14 Mojave Cross-Compile

Build Release License: MIT

Download Latest Release

Cross-compile Tailscale static binaries (tailscale + tailscaled) for macOS 10.14 Mojave (darwin/amd64) on a Linux host.

Why

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.

The Problem

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

What Doesn't Work

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

The Solution: Go -overlay Patch

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

Patched Files

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

Key Code Change in root_darwin.go

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: SecTrustGetCertificateAtIndex uses "Get" semantics (borrowed ref) — no CFRelease needed. SecTrustCopyCertificateChain uses "Copy" semantics (caller owns) — requires CFRelease. This follows Apple's Core Foundation memory management rules.

Prerequisites

  • Go 1.23+ (tested with 1.23 and 1.26)
  • git
  • Python 3 (for verification script)
  • Linux build host (cross-compile)

Go Version Compatibility

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+ overlay

Check detected version:

make check-go

Build

make

Or use the build script directly:

./build_tailscale-1.76.3_macOS_mojave.sh

Output binaries are written to mojave_amd64/.

Verify

make verify

Validates 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)

Deploy to Mojave

scp mojave_amd64/{tailscale,tailscaled,run_tailscale.sh} user@mojave-host:~/

On the Mojave machine:

sudo ./run_tailscale.sh

Socket Path Issue

After 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"

Project Structure

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.json is generated at build time with correct absolute paths. It is not tracked in git to avoid leaking local filesystem paths.

Key Lessons

  1. CGO_ENABLED=0 on darwin ≠ zero system dependency — Go loads Security.framework at runtime via //go:cgo_import_dynamic + assembly trampoline
  2. MACOSX_DEPLOYMENT_TARGET cannot fix symbol issues — it only changes the Mach-O header, not which APIs the code references
  3. Go -overlay is the correct way to patch stdlib — no need to fork Go or modify $GOROOT
  4. Apple Core Foundation memory rules matterCopy/Create = caller must CFRelease; Get = borrowed, no release needed

Development

Pre-commit hooks are configured. To set up:

python3 -m venv venv
source venv/bin/activate
pip install pre-commit
pre-commit install

About

Cross-compile Tailscale v1.76.3 for macOS 10.14 Mojave — patch Go stdlib via -overlay to replace macOS 12+ API with Mojave-compatible equivalents

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages