Initial commit

This commit is contained in:
2026-05-28 23:03:21 +02:00
commit d912a450bf
5 changed files with 796 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Build artifacts produced by build.sh --tar
/dist/
+237
View File
@@ -0,0 +1,237 @@
# syntax=docker/dockerfile:1
# =============================================================================
# Multi-architecture build
# =============================================================================
# Supported MikroTik Container architectures (build with `docker buildx`):
# linux/amd64 x86 / CHR
# linux/arm64 RB5009, CCR2xxx, hAP ax3, L009, Chateau (most modern)
# linux/arm/v7 ARMv7: hAP ac2, RB3011, RB4011, RB1100AHx4
#
# NOT supported here: ARMv5 (hEX Refresh / hAP ax S, EN7562CT CPU). ARMv5 has
# no Alpine/musl base image, so it cannot use the musl + scratch design below;
# it would need a glibc (Debian) base and produces a much larger image. See
# README for details if you need it.
#
# The Go builder cross-compiles, so it always runs NATIVELY on the build host
# ($BUILDPLATFORM) for speed; only the busybox stage and the final image run on
# the target platform.
# =============================================================================
# Stage 1: Build Tailscale combined binary (cross-compiled, runs natively)
# =============================================================================
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
ARG TAILSCALE_VERSION=v1.98.3
# Provided automatically by buildx for the target platform.
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN apk add --no-cache \
git \
upx \
ca-certificates
# Clone the exact release tag (no full history)
RUN git clone --depth 1 --branch ${TAILSCALE_VERSION} \
https://github.com/tailscale/tailscale.git /src/tailscale
WORKDIR /src/tailscale
# Build a minimal combined binary (tailscale CLI + tailscaled daemon in one file).
#
# Tag strategy — ALLOWLIST, not blocklist:
# 1. cmd/featuretags --min --add=osrouter generates the full ts_omit_* set
# (identical to build_dist.sh --extra-small), omitting every optional feature.
# 2. We pipe that through sed to REMOVE the ts_omit_ tags for the features
# we explicitly want, leaving everything else omitted.
# 3. We prepend ts_include_cli (combined daemon+CLI binary).
#
# This means any NEW ts_omit_* tag added in a future Tailscale release will
# automatically be omitted — we only get features we consciously opt into.
#
# Features opted in (removed from the omit list):
# advertiseexitnode — run as exit node for the tailnet
# advertiseroutes — advertise LAN subnets to the tailnet
# useexitnode — route router's own traffic via a remote exit node
# useroutes — accept routes advertised by other tailnet nodes
# dns — MagicDNS; configure MikroTik DNS to forward
# *.ts.net → 100.100.100.100; use --no-dns daemon
# flag to skip writing /etc/resolv.conf
# portmapper — NAT-PMP / PCP / UPnP to punch through upstream NAT
# listenrawdisco — raw sockets for more robust disco/NAT-traversal
# health — health subsystem required by 'tailscale status'
# cachenetmap — cache netmap on disk for faster reconnect after reboot
# IMPORTANT: mount cache dir on tmpfs, not internal flash
# iptables — Linux iptables support for routing rules
#
# Everything else remains omitted, including (rationale):
# clientupdate — updates managed via Docker image rebuild
# logtail — no persistent log writes to flash; also pass
# --no-logs-no-support at runtime
# netstack+gro — userspace networking; router uses kernel TUN
# ssh — not needed; access via MikroTik SSH + tailscale CLI
# all GUI/desktop/cloud/k8s features — irrelevant for a headless router
RUN mkdir -p /out && \
ALL_OMIT=$(GOOS= GOARCH= go run ./cmd/featuretags --min --add=osrouter) && \
TAGS=$(echo "ts_include_cli,${ALL_OMIT}" | \
sed \
-e 's/ts_omit_advertiseexitnode,\{0,1\}//g' \
-e 's/ts_omit_advertiseroutes,\{0,1\}//g' \
-e 's/ts_omit_useexitnode,\{0,1\}//g' \
-e 's/ts_omit_useroutes,\{0,1\}//g' \
-e 's/ts_omit_dns,\{0,1\}//g' \
-e 's/ts_omit_portmapper,\{0,1\}//g' \
-e 's/ts_omit_listenrawdisco,\{0,1\}//g' \
-e 's/ts_omit_health,\{0,1\}//g' \
-e 's/ts_omit_cachenetmap,\{0,1\}//g' \
-e 's/ts_omit_iptables,\{0,1\}//g' \
-e 's/,$//' \
) && \
echo "Build tags: ${TAGS}" && \
# Map Docker's TARGETARCH/TARGETVARIANT to Go's GOARCH/GOARM.
# For arm/v7 -> GOARM=7 (hardfloat). Other arches leave GOARM unset.
GOARM="" && \
if [ "${TARGETARCH}" = "arm" ]; then \
case "${TARGETVARIANT}" in \
v7) GOARM=7 ;; \
v6) GOARM=6 ;; \
v5) GOARM=5 ;; \
*) GOARM=7 ;; \
esac; \
fi && \
echo "Cross-compiling: GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GOARM=${GOARM}" && \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GOARM=${GOARM} \
go build \
-tags "${TAGS}" \
-gcflags="all=-l" \
-ldflags="-s -w" \
-trimpath \
-o /out/tailscale.combined \
./cmd/tailscaled
# Compress with UPX LZMA.
# Expected: ~14 MB raw → ~3.8 MB compressed (with -gcflags=all=-l)
RUN upx --lzma --best /out/tailscale.combined
# =============================================================================
# Stage 2: Custom minimal busybox
# =============================================================================
# The official busybox:musl image ships all ~404 applets at ~1.24 MB. For a
# debug shell on a flash-constrained router we only need ~100 applets, so we
# build a static busybox from source with a curated applet set, then UPX it
# down to ~230 kB on disk.
#
# UPX is normally dangerous with busybox: the ash shell's standalone applet
# dispatch re-execs /proc/self/exe, which UPX breaks, so typed commands fail
# (https://github.com/upx/upx/issues/248, closed as "invalid"). We sidestep
# this by building WITHOUT the standalone/nofork features (see
# busybox-applets.config) and providing an explicit /bin/<applet> symlink
# farm. Commands then resolve via the ordinary PATH -> symlink -> argv[0]
# dispatch, which works fine under UPX. The cost is a fork+exec per command,
# acceptable for an occasional debug shell. RouterOS stores the EXTRACTED
# rootfs on disk (overlayfs), so the ~190 kB UPX saving is real on-disk space.
#
# This stage runs on the TARGET platform (no --platform override): gcc then
# produces native target-arch binaries directly. Under buildx this is
# transparently emulated via binfmt/QEMU for non-native targets.
FROM alpine:3.21 AS busybox
ARG BUSYBOX_VERSION=1.37.0
RUN apk add --no-cache build-base linux-headers wget bzip2 perl upx
RUN wget -q https://busybox.net/downloads/busybox-${BUSYBOX_VERSION}.tar.bz2 \
&& tar xf busybox-${BUSYBOX_VERSION}.tar.bz2
WORKDIR /busybox-${BUSYBOX_VERSION}
# allnoconfig = every feature OFF; then enable only the curated applet set.
COPY busybox-applets.config /tmp/applets.config
RUN make allnoconfig && \
while read -r sym; do \
case "$sym" in ''|\#*) continue ;; esac; \
if grep -q "^# CONFIG_${sym} is not set" .config; then \
sed -i "s/^# CONFIG_${sym} is not set/CONFIG_${sym}=y/" .config; \
elif ! grep -q "^CONFIG_${sym}=y" .config; then \
echo "CONFIG_${sym}=y" >> .config; \
fi; \
done < /tmp/applets.config && \
yes "" | make oldconfig >/dev/null 2>&1 && \
make -j"$(nproc)" >/dev/null 2>&1 && \
strip busybox
# Lay out a minimal rootfs with busybox + an applet symlink per applet.
# Symlinks (argv[0] dispatch) are how busybox selects an applet and make the
# applets resolvable via $PATH from inside the shell. We derive the applet
# names from the build .config: a symbol is an applet if its lowercase name
# resolves to a runnable applet (busybox returns "applet not found" on stderr
# for non-applet symbols like FEATURE_* / STATIC, which we filter out).
# We generate symlinks from the UNCOMPRESSED binary (so the probe is reliable),
# then UPX-compress the binary in place afterwards.
RUN mkdir -p /rootfs/bin && \
grep '^CONFIG_.*=y' .config \
| sed -e 's/^CONFIG_//' -e 's/=y$//' \
| tr 'A-Z' 'a-z' \
| while read -r app; do \
if ! ./busybox "$app" --help 2>&1 | grep -q "applet not found"; then \
ln -sf /bin/busybox /rootfs/bin/"$app"; \
fi; \
done && \
ln -sf /bin/busybox /rootfs/bin/sh && \
echo "Applet symlinks created: $(ls /rootfs/bin | wc -l)" && \
upx --lzma --best busybox && \
cp busybox /rootfs/bin/busybox
# =============================================================================
# Stage 3: Final runtime image
# =============================================================================
FROM scratch
# Custom static busybox + applet symlinks (provides /bin/sh and utilities)
COPY --from=busybox /rootfs/ /
# CA certificates (needed to reach Tailscale coordination server)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Combined Tailscale binary
COPY --from=builder /out/tailscale.combined /usr/local/bin/tailscale.combined
# Symlinks: combined binary behavior switches on argv[0]
RUN ["/bin/busybox", "ln", "-s", "/usr/local/bin/tailscale.combined", "/usr/local/bin/tailscale"]
RUN ["/bin/busybox", "ln", "-s", "/usr/local/bin/tailscale.combined", "/usr/local/bin/tailscaled"]
# Ensure /usr/local/bin and busybox dirs are on PATH for interactive shells
ENV PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# -----------------------------------------------------------------------------
# Volume layout (to be created by deploy script):
#
# /var/lib/tailscale — persistent state (authkey, node identity)
# → bind-mount to MikroTik disk storage
# → survives reboots, written infrequently
#
# /var/lib/tailscale/cache — netmap cache (cachenetmap feature)
# → mount as tmpfs so it never touches flash
# → speeds up reconnect but is recreatable
#
# /var/run/tailscale — runtime socket dir
# → tmpfs, lost on reboot (expected)
# -----------------------------------------------------------------------------
VOLUME ["/var/lib/tailscale"]
ENTRYPOINT ["/usr/local/bin/tailscaled"]
# Default flags:
# --no-logs-no-support disables logtail uploads (logtail binary code is
# omitted, but the flag also suppresses any remaining
# log buffering and prevents the daemon from trying
# to write log files)
# --state persistent node identity / authkey storage
# --socket CLI communication socket (on tmpfs)
# --statedir where cache and other runtime files land
CMD ["--no-logs-no-support", \
"--state=/var/lib/tailscale/tailscaled.state", \
"--socket=/var/run/tailscale/tailscaled.sock", \
"--statedir=/var/lib/tailscale"]
+285
View File
@@ -0,0 +1,285 @@
# mikrotik-tailscale
A minimal Tailscale Docker image built for MikroTik routers running
[Container](https://help.mikrotik.com/docs/display/ROS/Container). Fits in
16 MB internal flash. Built from source with only router-relevant features
included.
## Supported architectures
| Docker platform | RouterOS arch | Example devices |
|---|---|---|
| `linux/amd64` | x86 / CHR | x86 installs, Cloud Hosted Router |
| `linux/arm64` | arm64 | RB5009, CCR2004/2116/2216, hAP ax³, L009, Chateau |
| `linux/arm/v7` | arm (ARMv7) | hAP ac², RB3011, RB4011, RB1100AHx4 |
A single Dockerfile builds all three. The Go binary is **cross-compiled** (the
builder stage runs natively on the host for speed), while the busybox stage and
final image are built for the target platform (via `buildx` + QEMU/binfmt for
non-native targets).
**ARMv5 is not supported** (hEX Refresh / hAP ax S, EN7562CT CPU — RouterOS
calls these `arm32v5`). ARMv5 has no Alpine/musl base image, so it cannot use
this image's musl + `scratch` design; it would require a glibc (Debian) base
and produce a substantially larger image (~50 MB+ vs ~4 MB). If you need it,
that's a separate build, not just a `--platform` change.
## Image size
On-disk footprint once extracted (this is what matters — RouterOS stores the
**extracted** rootfs on disk via overlayfs, not the compressed layers):
| Component | On-disk size |
|---|---|
| tailscale.combined (UPX-compressed) | ~3.84 MB |
| custom static busybox (UPX, ~100 applets) | ~229 kB |
| CA certificates | ~218 kB |
| **Total extracted rootfs** | **~4.1 MB** |
(The compressed image / transfer tarball is ~4.3 MB.)
The binary is built with Tailscale's `--extra-small` feature tag set as the
baseline. Features are opted in explicitly — any new feature Tailscale adds
in a future release stays omitted until deliberately added to the Dockerfile.
### Size optimizations applied
- **Feature allowlist** (`--extra-small` baseline + ~10 opt-ins) keeps the
binary minimal and forward-safe against new Tailscale features.
- **`-gcflags=all=-l`** disables function inlining across all packages,
shrinking the compressed binary by ~600 kB. Inlining is a performance
optimization only; disabling it does not affect correctness. The CPU cost
is negligible for an I/O-bound router daemon.
- **`-ldflags="-s -w"`** strips the symbol table and DWARF debug info.
- **`-trimpath`** removes local filesystem paths from the binary.
- **UPX `--lzma --best`** compresses the Tailscale binary (~14 MB → ~3.8 MB).
- **Custom static busybox** — instead of the official `busybox:musl` image
(all ~404 applets, ~1.24 MB), a static busybox is built from source with
only ~100 curated applets (~420 kB), then UPX-compressed to ~229 kB on
disk. The applet set is defined in
[`busybox-applets.config`](busybox-applets.config).
**busybox UPX requires care.** UPX normally breaks busybox's standalone
applet dispatch: the ash shell re-execs `/proc/self/exe` to run built-in
applets, and UPX breaks that path so typed commands fail
([upx#248](https://github.com/upx/upx/issues/248), closed as "invalid").
We work around it by building **without** the standalone/nofork features
and providing an explicit `/bin/<applet>` symlink farm. Commands then
resolve via the normal `PATH` → symlink → `argv[0]` dispatch, which works
under UPX. The cost is a `fork+exec` per command instead of a nofork
internal call — fine for an occasional debug shell.
Because RouterOS stores the extracted rootfs on disk, UPX'ing busybox
saves a real ~195 kB of flash (424 kB → 229 kB), not just transfer size.
The final image is built `FROM scratch` — there is no base distro layer.
It contains only the busybox binary + applet symlinks, the CA bundle, and
the Tailscale binary.
## Features included
| Feature | Why |
|---|---|
| `advertise-exit-node` | Run the router as a Tailscale exit node |
| `advertise-routes` | Expose LAN subnets to the tailnet |
| `use-exit-node` | Route the router's own traffic via a remote exit node |
| `accept-routes` | Receive subnet routes from other tailnet nodes |
| DNS / MagicDNS | Resolve `*.ts.net` names (see DNS section below) |
| portmapper (NAT-PMP/PCP/UPnP) | Punch through upstream NAT |
| listenrawdisco | Raw socket disco for better NAT traversal |
| health | Powers `tailscale status` output |
| cachenetmap | Cache network map for faster reconnect after reboot |
| iptables | Linux iptables support for routing rules |
| osrouter | Configure kernel network stack and routing tables |
## Features intentionally omitted
| Feature | Reason |
|---|---|
| `clientupdate` | Updates are managed by rebuilding the Docker image |
| `logtail` | Would attempt persistent log writes; wear flash |
| `netlog` | Network flow logging; separate concern |
| `netstack` + `gro` | Userspace/gVisor networking; router uses kernel TUN |
| `ssh` | Access via MikroTik SSH + `tailscale` CLI instead |
| `linuxdnsfight` | inotify on `/etc/resolv.conf`; no systemd in container |
| `networkmanager` / `resolved` / `dbus` / `sdnotify` | No systemd stack in container |
| `drive` / `taildrop` / `webclient` | Not useful on a headless router |
| All GUI / desktop / cloud / k8s features | Irrelevant |
## Volume layout
Three mount points, with different persistence requirements:
```
/var/lib/tailscale persistent — node identity, auth state
bind-mount to MikroTik disk storage
written rarely (only on auth / key rotation)
/var/lib/tailscale/cache ephemeral — netmap cache
mount as tmpfs to avoid flash writes
recreated automatically on next connect
/var/run/tailscale ephemeral — daemon Unix socket
mount as tmpfs
lost on reboot, recreated on start
```
Keeping the cache and socket directories on tmpfs prevents unnecessary
flash wear while still allowing fast reconnect after reboot (the cache
is repopulated from the Tailscale coordination server on first connect).
## Building
### All architectures at once
Use the helper script (requires `docker buildx` + QEMU/binfmt for non-native
targets):
```sh
# One-time: register emulators for cross-arch builds
docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
# Build all arches and load into local docker
./build.sh
# Build all arches and also export per-arch tarballs into ./dist/
./build.sh --tar
# Build a single arch
./build.sh arm64
./build.sh --tar armv7
```
### Manual single-arch build
The architecture is selected via `buildx --platform`; the Dockerfile maps it to
the correct `GOARCH`/`GOARM` automatically:
```sh
docker buildx build --platform linux/arm64 --load -t mikrotik-tailscale:arm64 .
docker buildx build --platform linux/arm/v7 --load -t mikrotik-tailscale:armv7 .
docker buildx build --platform linux/amd64 --load -t mikrotik-tailscale:amd64 .
```
To build for a different Tailscale version, add:
```sh
--build-arg TAILSCALE_VERSION=v1.98.3
```
### Notes
- The Go builder cross-compiles natively (fast); only the busybox stage runs
under emulation for non-native targets.
- The build prints the resolved target and Go build tags, e.g.:
```
Cross-compiling: GOOS=linux GOARCH=arm64 GOARM=
Build tags: ts_include_cli,ts_omit_ace,ts_omit_acme,...
```
### Per-architecture image sizes
| Arch | Image |
|---|---|
| amd64 | ~4.2 MB |
| arm64 | ~3.5 MB |
| arm/v7 | ~3.5 MB |
## Running (local test)
```sh
# Create a volume for persistent state
docker volume create tailscale-state
# Start the daemon
docker run -d \
--name tailscale \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
--device /dev/net/tun \
--tmpfs /var/lib/tailscale/cache \
--tmpfs /var/run/tailscale \
-v tailscale-state:/var/lib/tailscale \
mikrotik-tailscale
# Authenticate (opens browser / prints auth URL)
docker exec tailscale tailscale login
# Check status
docker exec tailscale tailscale status
# Advertise a subnet
docker exec tailscale tailscale set --advertise-routes=192.168.88.0/24
# Advertise as exit node
docker exec tailscale tailscale set --advertise-exit-node
```
Subnet routes and exit node advertisement must also be approved in the
[Tailscale admin console](https://login.tailscale.com/admin/machines).
## Unattended authentication
For automated / headless deployment, use an auth key:
```sh
docker exec tailscale tailscale up \
--authkey=tskey-auth-<key> \
--advertise-routes=192.168.88.0/24 \
--advertise-exit-node
```
Auth keys can be created in the Tailscale admin console under
**Settings → Keys**. Use a reusable key tagged with a device tag for
infrastructure nodes.
## MagicDNS
The binary includes DNS support but the daemon is started with
`--no-logs-no-support`, which does not affect DNS. To use MagicDNS name
resolution, configure MikroTik's DNS to forward `.ts.net` queries to
Tailscale's magic DNS resolver:
```
/ip dns static
add name="ts.net" type=FWD forward-to=100.100.100.100 match-subdomain=yes
```
This avoids writing to `/etc/resolv.conf` inside the container (which would
happen if `--accept-dns` is passed to `tailscale up`). The container resolves
Tailscale node names; the rest of the router uses its own DNS.
## Flash wear protection
Several measures are in place to avoid wearing out internal flash:
- `clientupdate` omitted from binary — no background update downloads
- `logtail` omitted from binary — no log upload attempts
- `--no-logs-no-support` passed to daemon — suppresses any remaining log
buffering
- `netmap` cache mounted on tmpfs — cache writes never reach flash
- `/var/run/tailscale` socket on tmpfs — runtime files never reach flash
- Only `/var/lib/tailscale/tailscaled.state` touches persistent storage,
and it is written only when the node authenticates or rotates its key
## Upgrading
Update the `TAILSCALE_VERSION` build arg and rebuild the image. The feature
allowlist in the Dockerfile carries forward automatically — any new
`ts_omit_*` tags introduced in the new version will be omitted by default.
```sh
TAG=v1.99.0 ./build.sh --tar # rebuild all arches with the new version
# or, single arch:
docker buildx build --platform linux/arm64 \
--build-arg TAILSCALE_VERSION=v1.99.0 \
--load -t mikrotik-tailscale:v1.99.0-arm64 .
```
## References
- [Tailscale: Smaller binaries for embedded devices](https://tailscale.com/docs/how-to/set-up-small-tailscale)
- [MikroTik Container documentation](https://help.mikrotik.com/docs/display/ROS/Container)
- [Tailscale subnet routers](https://tailscale.com/kb/1019/subnets)
- [Tailscale exit nodes](https://tailscale.com/kb/1103/exit-nodes)
Executable
+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
# Build mikrotik-tailscale images for all supported MikroTik architectures.
#
# Produces one OCI image per architecture and, optionally, a per-arch tarball
# suitable for `/container/add file=...` on RouterOS.
#
# Usage:
# ./build.sh # build all arches, load into local docker
# ./build.sh arm64 # build a single arch
# ./build.sh --tar # build all arches and export .tar files
# ./build.sh --tar arm64 # build one arch and export its .tar
#
# Requirements:
# - docker with buildx
# - For non-native targets: binfmt/QEMU emulators registered, e.g.:
# docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
set -eu
IMAGE="${IMAGE:-mikrotik-tailscale}"
TAG="${TAG:-latest}"
OUTDIR="${OUTDIR:-dist}"
# MikroTik Container supported architectures (Docker platform -> tag suffix).
# ARMv5 (hEX Refresh / hAP ax S) is intentionally excluded; it has no musl
# base and needs a separate glibc build — see README.
PLATFORMS="linux/amd64:amd64 linux/arm64:arm64 linux/arm/v7:armv7"
make_tar=0
only_arch=""
for arg in "$@"; do
case "$arg" in
--tar) make_tar=1 ;;
-*) echo "unknown flag: $arg" >&2; exit 1 ;;
*) only_arch="$arg" ;;
esac
done
build_one() {
platform="$1"
suffix="$2"
ref="${IMAGE}:${TAG}-${suffix}"
echo ">>> Building ${ref} for ${platform}"
set -- --platform "${platform}" --load -t "${ref}"
if [ -n "${TAILSCALE_VERSION:-}" ]; then
set -- "$@" --build-arg "TAILSCALE_VERSION=${TAILSCALE_VERSION}"
fi
docker buildx build "$@" .
if [ "${make_tar}" -eq 1 ]; then
mkdir -p "${OUTDIR}"
out="${OUTDIR}/${IMAGE}-${TAG}-${suffix}.tar"
echo ">>> Exporting ${out}"
docker save "${ref}" -o "${out}"
echo " $(ls -l "${out}" | awk '{printf "%.1f MB", $5/1048576}') ${out}"
fi
}
for entry in $PLATFORMS; do
platform="${entry%%:*}"
suffix="${entry##*:}"
if [ -n "${only_arch}" ] && [ "${only_arch}" != "${suffix}" ]; then
continue
fi
build_one "${platform}" "${suffix}"
done
echo ">>> Done."
echo "Images:"
docker images "${IMAGE}" --format ' {{.Repository}}:{{.Tag}}\t{{.Size}}'
+202
View File
@@ -0,0 +1,202 @@
# Curated busybox applet set for a Tailscale-on-MikroTik debug shell.
#
# This file is consumed by the Dockerfile's busybox build stage. It starts
# from `make allnoconfig` (everything OFF) and turns ON only the symbols
# listed here, keeping the binary small (~420 kB static vs ~1.24 MB for the
# full official busybox).
#
# Format: one CONFIG symbol per line (without the CONFIG_ prefix or =y).
# Lines starting with # and blank lines are ignored.
#
# IMPORTANT: this busybox is intentionally NOT UPX-compressed. UPX breaks
# busybox's internal applet dispatch from the ash shell — typed commands
# fall through to a $PATH lookup instead of running the built-in applet.
# See https://github.com/upx/upx/issues/248 (closed as "invalid"; it is a
# busybox/UPX interaction the UPX project will not fix). A custom static
# build is both smaller than a UPX'd full busybox AND avoids this entirely.
# --- Static build (runs anywhere, no dynamic loader) ---
STATIC
# --- Large File Support (64-bit off_t) ---
# musl (Alpine) always uses a 64-bit off_t. Without LFS, busybox's off_t size
# self-check (BUG_off_t_size_is_misdetected) fails to compile on 32-bit targets
# such as arm/v7 — especially under QEMU emulation. Enabling LFS is correct for
# musl on every architecture, so we set it unconditionally.
LFS
# --- Shell: ash, standalone mode so typed commands resolve to applets ---
ASH
SH_IS_ASH
ASH_INTERNAL_GLOB
ASH_BASH_COMPAT
ASH_JOB_CONTROL
ASH_ALIAS
ASH_GETOPTS
ASH_CMDCMD
ASH_ECHO
ASH_PRINTF
ASH_TEST
ASH_HELP
ASH_OPTIMIZE_FOR_SIZE
# NOTE: FEATURE_SH_STANDALONE, FEATURE_PREFER_APPLETS and FEATURE_SH_NOFORK
# are intentionally LEFT OFF (they are off by default in allnoconfig).
#
# Those features make the shell run applets internally by re-exec'ing
# /proc/self/exe instead of doing a normal PATH lookup. That /proc/self/exe
# path is exactly what UPX breaks (https://github.com/upx/upx/issues/248):
# under UPX the shell fails to find its own applets and falls through to a
# (nonexistent) PATH binary.
#
# By leaving them off, typed commands resolve via the ordinary PATH ->
# /bin/<applet> symlink -> busybox argv[0] dispatch, which works correctly
# even when the busybox binary IS UPX-compressed. This lets us UPX busybox
# (~424 kB -> ~230 kB on-disk) without breaking the shell. The cost is a
# fork+exec per command instead of a nofork internal call, which is fine
# for an occasional debug shell.
FEATURE_EDITING
FEATURE_EDITING_HISTORY
FEATURE_TAB_COMPLETION
FEATURE_SUID
LONG_OPTS
# --- Coreutils ---
LS
FEATURE_LS_FILETYPES
FEATURE_LS_SORTFILES
FEATURE_LS_TIMESTAMPS
FEATURE_LS_USERNAME
FEATURE_LS_COLOR
CAT
ECHO
PRINTF
PWD
TRUE
FALSE
TEST
MKDIR
RMDIR
RM
MV
CP
LN
TOUCH
STAT
READLINK
REALPATH
BASENAME
DIRNAME
CHMOD
CHOWN
CHGRP
HEAD
FEATURE_FANCY_HEAD
TAIL
FEATURE_FANCY_TAIL
WC
SORT
FEATURE_SORT_BIG
UNIQ
CUT
TR
EXPR
SEQ
SLEEP
YES
ENV
PRINTENV
WHICH
WHOAMI
ID
DATE
HOSTNAME
UNAME
MKTEMP
# --- Process / system inspection ---
PS
FEATURE_PS_WIDE
DESKTOP
TOP
FEATURE_TOP_INTERACTIVE
FEATURE_TOP_CPU_USAGE_PERCENTAGE
KILL
KILLALL
PIDOF
PGREP
PKILL
FREE
UPTIME
NPROC
DMESG
WATCH
# --- Text tools ---
GREP
FEATURE_GREP_CONTEXT
FEATURE_GREP_EGREP_ALIAS
EGREP
FGREP
SED
AWK
FIND
FEATURE_FIND_TYPE
FEATURE_FIND_PERM
FEATURE_FIND_MTIME
FEATURE_FIND_NEWER
FEATURE_FIND_EXEC
XARGS
HEXDUMP
OD
STRINGS
LESS
MORE
CMP
DIFF
VI
# --- Networking (ip is the one command Tailscale shells out to) ---
IP
FEATURE_IP_ADDRESS
FEATURE_IP_LINK
FEATURE_IP_ROUTE
FEATURE_IP_NEIGH
FEATURE_IP_RULE
FEATURE_IP_TUNNEL
IPADDR
IPLINK
IPROUTE
IPNEIGH
IPRULE
PING
PING6
FEATURE_FANCY_PING
NSLOOKUP
NETSTAT
ARP
ARPING
WGET
TRACEROUTE
FEATURE_IPV6
# --- Filesystem ---
MOUNT
UMOUNT
DF
DU
SYNC
LSOF
TAR
GZIP
GUNZIP
ZCAT
FEATURE_SEAMLESS_GZ
# --- Misc shell conveniences ---
CLEAR
RESET
TTY
SETSID
NOHUP
TIMEOUT
FLOCK