Shrinking Tailscale by 75%: Building an Ultra-Lean Exit Node for OpenWrt Routers
If you’ve ever tried running Tailscale on a flash-constrained router like the GL.iNet Slate AX (AXT1800), you’ve likely run into a frustrating storage crunch.
Out of the box, the pre-installed standard Go binary for tailscaled is massive—weighing in at ~23.2 MB. On embedded systems running OpenWrt where overlay flash space is measured in tens of megabytes, a single binary taking up that much space is a heavy tax.
To make matters worse, GL.iNet doesn’t push frequent package updates for Tailscale via standard firmware releases. If you rely solely on stock repositories, you can find yourself stuck running an outdated version that misses out on crucial performance improvements, protocol tweaks, and security patches.
In this guide, we’ll walk through how to compile Tailscale directly from source. Not only will this shrink the binary from 23.2 MB down to just 5.8 MB (~75% space saved), but it will also give you total freedom to run the absolute latest upstream release whenever you want.
🛠️ The Game Plan
To achieve maximum compression and stay perpetually up-to-date, we use three key techniques:
- Building from Source: Bypass firmware release cycles and compile the latest upstream Tailscale commit directly.
- Stripping Unused Features & Multicall Architecture: Use custom Go build tags to strip out unnecessary server/GUI components and embed the CLI functionality directly into a single multicall binary (
tailscaled). - Aggressive UPX Compression: Compress the stripped Go binary payload using
upx --lzma --best.
🧰 Prerequisites
- A host machine with Go 1.20+ installed (e.g., macOS / Linux).
- UPX installed (
brew install upxon macOS orsudo apt install upxon Linux). - SSH access to your OpenWrt router.
Step 1: Clone or Update the Tailscale Repository
Grab the latest official Tailscale source code on your build machine (or git pull if you already have it cloned):
git clone https://github.com/tailscale/tailscale.git
cd tailscale(Pro tip: If you want a specific release version, you can run git checkout v1.x.x before building!)
Step 2: Compile the Lean Multicall Binary
We will cross-compile for ARM64 (GOARCH=arm64 for the AXT1800) and pass a tailored set of build tags.
💡 Pro-Tip: Verify Your Router’s Architecture with uname
Before cross-compiling, it’s always a good idea to confirm your router’s exact CPU architecture by running this command via SSH:
uname -m- For the GL.iNet Slate AX (AXT1800), this will return
aarch64, which maps directly toGOARCH=arm64during your Go build. - If you are targeting other popular OpenWrt travel routers (such as older ARMv7 models),
uname -mmight returnarmv7l, requiringGOARCH=armandGOARM=7.
⚠️ Crucial Build Tag Note:
Here is the detailed breakdown of all the Go build tags used in the compilation command:
advertiseexitnode(Exit Node Functionality) Enables the router's capability to act as a Tailscale exit node, allowing other devices on your tailnet to route all their internet traffic through it.advertiseroutes(Subnet Routing) Allows the router to advertise physical local network subnets (e.g., your home LAN) so remote nodes can access local devices behind the router.osrouter(Network Routing) Integrates with the host operating system's native routing tables and IP stack management mechanisms.iptables(Firewall & Packet Filtering) Enables manipulation of Linuxiptablesrules, which Tailscale requires to configure network address translation (NAT) and manage packet flows for exit nodes and subnet routing.nattraversal(Connectivity) Includes STUN, TURN, and ICE logic required to establish peer-to-peer (P2P) UDP connections through NAT firewalls and routers.udptransport(Data Plane Transport) Compiles the custom UDP packet transport layer used by Tailscale to exchange encrypted frames over the wire.gro(Performance Optimization) Enables Generic Receive Offload (GRO) support inside user-space netstack, significantly boosting network throughput performance on Linux.netstack(Networking Stack) Compiles Tailscale's built-in userspace TCP/IP stack (netstack), which handles virtual networking interfaces without heavy reliance on system kernel tun/tap devices.dns(Name Resolution) Handles MagicDNS and internal name resolution routing so nodes can resolve hostnames seamlessly across your tailnet.clientmetrics(Diagnostics & Telemetry) Compiles internal telemetry and metrics collection hooks used to monitor connection performance and health.logtail(Logging Infrastructure) Bundles the logtail client package to securely stream diagnostic logs and connection errors to backend infrastructure if configured.health(System Health Checks) Includes internal health monitors that track network connectivity states, DERP relays, and warning conditions.wtun(WireGuard Tunnel) Compiles the userspace WireGuard packet encapsulation and decryption engine (wtun), which forms the cryptographic core of Tailscale.ts_include_cli(Command-Line Interface — Crucial for Multicall) Explicitly embeds the CLI command parser inside the daemon binary. This allows a single compiled executable (tailscaled) to recognize execution names via symlinks (liketailscale statusandtailscale version) instead of throwing a non-flag error.
Run the build command on your host system:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
-trimpath \
-gcflags="all=-l" \
-tags="advertiseexitnode,advertiseroutes,osrouter,iptables,nattraversal,udptransport,gro,netstack,dns,clientmetrics,logtail,health,wtun,ts_include_cli" \
-ldflags="-s -w -buildid=" \
-o tailscale.multicall ./cmd/tailscaledWhat do these flags do?
-trimpath: Removes absolute local directory paths from the compiled binary.-ldflags="-s -w -buildid=": Strips the ELF build ID, DWARF debugging information, and symbol tables.-tags="...,ts_include_cli": Includes networking, routing, exit node functionality, and embeds the CLI handler insidetailscaled.
Step 3: Compress with UPX
Next, run UPX with LZMA compression to crush the binary size down to under 6 MB:
upx --lzma --best tailscale.multicallYou should see output showing your binary size drop from its stripped state down to ~5.8 MB!
Step 4: Backup the Stock Binary on Your Router
Before touching any production files on your router, take a local backup of the stock binary.
Get Aalok Kamble’s stories in your inbox
Join Medium for free to get updates from this writer.
Run this command from your host machine:
# Modern SSH on macOS uses SFTP by default; use -O for OpenWrt's Dropbear SSH
scp -O root@192.168.8.1:/usr/sbin/tailscaled ./tailscaled.stock.bakNow you have a safety net if you ever need to revert back.
Step 5: Transfer and Deploy the Lean Binary
1. Copy the new binary to the router
From your host machine:
scp -O tailscale.multicall root@192.168.8.1:/usr/sbin/tailscale.multicall2. Configure symlinks and restart the service on the router
SSH into your router (ssh root@192.168.8.1) and run the following commands:
# 1. Stop the active daemon
/etc/init.d/tailscale stop# 2. Replace the old binary with your new build
mv -f /usr/sbin/tailscale.multicall /usr/sbin/tailscaled
chmod +x /usr/sbin/tailscaled# 3. Create the multicall symlink for the CLI tool
ln -sf /usr/sbin/tailscaled /usr/sbin/tailscale# 4. Start the service
/etc/init.d/tailscale start
Step 6: Verification
Let’s test our work! Verify that the file size savings are locked in and the CLI operates as expected:
# Check binary size
ls -lh /usr/sbin/tailscaled
# Output: -rwxr-xr-x 1 root root 5.8M ...# Verify CLI functionality
tailscale version
tailscale statusBoth commands should return clean status responses without any errors, confirming you are running the newest upstream release.
📊 Results Summary
Stock GL.iNet Binary
- File Size: 23.2 MB
- Upstream Freshness: Dependent on manufacturer firmware cycles
Optimized Multicall + UPX
- File Size: 5.8 MB (~75% space saved)
- Upstream Freshness: Always up-to-date (Latest GitHub commit)
By building from source, you reclaim over 17 MB of precious router flash storage while completely bypassing manufacturer lag, ensuring your router runs the sharpest, newest version of Tailscale available.
Happy hacking! 🚀






