Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Build artifacts produced by build.sh --tar
|
||||||
|
/dist/
|
||||||
+237
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -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}}'
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user