Compare commits

..

1 Commits

Author SHA1 Message Date
Renovate Bot bfc4c44f01 chore(deps): update alpine docker tag to v3.24.0
ci/woodpecker/pr/pr-build Pipeline was successful
2026-06-10 02:00:58 +00:00
10 changed files with 43 additions and 551 deletions
+2 -15
View File
@@ -9,31 +9,18 @@
# Reports pass/fail status back to Gitea, so it shows up as a required check on # Reports pass/fail status back to Gitea, so it shows up as a required check on
# the PR. # the PR.
# Changes that can't affect the image don't trigger the build: docs and the
# RouterOS-side script (routeros/**: lives on the router, not in the image).
# NOTE: if Gitea is ever configured to REQUIRE this check for merging, a
# PR touching only excluded files will have no check at all — exempt such PRs
# or merge manually. Renovate PRs always touch the Dockerfile or pipeline
# files, so the automerge gate is unaffected by these exclusions.
when: when:
- event: pull_request - event: pull_request
path:
exclude: &non_image_paths
- '**/*.md'
- 'docs/**'
- 'routeros/**'
- 'renovate.json'
- event: push - event: push
branch: main branch: main
path:
exclude: *non_image_paths
steps: steps:
- name: Build all arches (no push) - name: Build all arches (no push)
image: woodpeckerci/plugin-docker-buildx:6.1.0 image: woodpeckerci/plugin-docker-buildx:6.1.0
privileged: true privileged: true
settings: settings:
repo: mikrotik-tailscale
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
dry_run: true dry-run: true
build_args: build_args:
- OCI_VERSION=ci-${CI_COMMIT_SHA} - OCI_VERSION=ci-${CI_COMMIT_SHA}
+1 -11
View File
@@ -13,19 +13,9 @@
# unchanged, so no tag is created and nothing is released — they ride along # unchanged, so no tag is created and nothing is released — they ride along
# with the next Tailscale bump or manual tag. # with the next Tailscale bump or manual tag.
# Skipped for pushes that can't introduce a new Tailscale version:
# TAILSCALE_VERSION lives in the Dockerfile, so a push touching only docs or
# the RouterOS-side script can never produce a new version to tag (the job
# would just no-op after spinning up OpenBao + git containers).
when: when:
- event: push - event: push
branch: main branch: main
path:
exclude:
- '**/*.md'
- 'docs/**'
- 'routeros/**'
- 'renovate.json'
steps: steps:
- name: Get git token from OpenBao - name: Get git token from OpenBao
@@ -44,7 +34,7 @@ steps:
- bao kv get -mount secret -field RENOVATE_TOKEN renovate > /woodpecker/git_token - bao kv get -mount secret -field RENOVATE_TOKEN renovate > /woodpecker/git_token
- name: Auto-tag mt.1 on Tailscale bump - name: Auto-tag mt.1 on Tailscale bump
image: alpine/git:v2.54.0 image: alpine/git:v2.52.0
environment: environment:
CI_REPO_URL: https://gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale.git CI_REPO_URL: https://gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale.git
commands: commands:
+3 -7
View File
@@ -46,7 +46,7 @@ steps:
- bao kv get -mount secret -field GITHUB_COM_TOKEN renovate > /woodpecker/github_com_token - bao kv get -mount secret -field GITHUB_COM_TOKEN renovate > /woodpecker/github_com_token
- name: renovate - name: renovate
# Renovate's built-in "woodpecker" manager tracks this image automatically. # Renovate's built-in "woodpecker" manager tracks this image automatically.
image: renovate/renovate:43.227.1 image: renovate/renovate:43.207.4
environment: environment:
# --- platform / target --- # --- platform / target ---
RENOVATE_PLATFORM: gitea RENOVATE_PLATFORM: gitea
@@ -58,12 +58,8 @@ steps:
# Use the committed renovate.json; don't open an onboarding PR. # Use the committed renovate.json; don't open an onboarding PR.
RENOVATE_ONBOARDING: "false" RENOVATE_ONBOARDING: "false"
RENOVATE_REQUIRE_CONFIG: "optional" RENOVATE_REQUIRE_CONFIG: "optional"
# Git identity for the branches/commits Renovate creates. MUST match the # Git identity for the branches/commits Renovate creates.
# bot's Gitea account email: platform actions (automerge merge commits, RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@localhost>"
# "update branch") are attributed to the account email, and Renovate
# flags branches containing commits from unrecognized emails as
# "edited by someone else" and stops rebasing them.
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate@lumpiasty.xyz>"
# GitHub token (read-only, no repo access) lets Renovate fetch release # GitHub token (read-only, no repo access) lets Renovate fetch release
# notes / changelogs and avoids GitHub API rate limits for the # notes / changelogs and avoids GitHub API rate limits for the
# github-releases datasource (tailscale). Optional but recommended. # github-releases datasource (tailscale). Optional but recommended.
+16 -182
View File
@@ -12,27 +12,14 @@
# it would need a glibc (Debian) base and produces a much larger image. See # it would need a glibc (Debian) base and produces a much larger image. See
# README for details if you need it. # README for details if you need it.
# #
# Both the Go (Tailscale) stage and the C (busybox) stage cross-compile: they # The Go builder cross-compiles, so it always runs NATIVELY on the build host
# always run NATIVELY on the build host ($BUILDPLATFORM) and produce binaries # ($BUILDPLATFORM) for speed; only the busybox stage and the final image run on
# for $TARGETPLATFORM. This eliminates QEMU emulation entirely from the build, # the target platform.
# which is the main source of slowness in multi-arch builds. Only the final
# scratch stage pulls in the target-arch-specific layers (CA certs, busybox
# rootfs) which are just file copies with no emulated execution.
#
# Cross-compilation for C (busybox) is provided by tonistiigi/xx, which
# configures clang+lld as a cross-compiler and installs musl headers for the
# target arch via xx-apk.
# =============================================================================
# xx: Dockerfile cross-compilation helpers (provides xx-clang, xx-apk, etc.)
# =============================================================================
# renovate: datasource=docker depName=tonistiigi/xx versioning=docker
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
# ============================================================================= # =============================================================================
# Stage 1: Build Tailscale combined binary (cross-compiled, runs natively) # Stage 1: Build Tailscale combined binary (cross-compiled, runs natively)
# ============================================================================= # =============================================================================
FROM --platform=$BUILDPLATFORM golang:1.26.4-alpine@sha256:f1ddd9fe14fffc091dd98cb4bfa999f32c5fc77d2f2305ea9f0e2595c5437c14 AS builder FROM --platform=$BUILDPLATFORM golang:1.26.4-alpine@sha256:f23e8b227fb4493eabe03bede4d5a32d04092da71962f1fb79b5f7d1e6c2a17f AS builder
# renovate: datasource=github-releases depName=tailscale packageName=tailscale/tailscale versioning=semver # renovate: datasource=github-releases depName=tailscale packageName=tailscale/tailscale versioning=semver
ARG TAILSCALE_VERSION=v1.98.5 ARG TAILSCALE_VERSION=v1.98.5
@@ -53,23 +40,6 @@ RUN git clone --depth 1 --branch ${TAILSCALE_VERSION} \
WORKDIR /src/tailscale WORKDIR /src/tailscale
# Inject a stderr verbosity filter into the tailscaled package.
#
# With logtail compiled out (ts_omit_logtail), tailscaled never installs
# logpolicy (see `if buildfeatures.HasLogTail` in cmd/tailscaled/tailscaled.go),
# so log output goes raw to stderr: the [v1]/[v2] verbosity tags embedded in
# messages are neither parsed nor filtered, and --verbose has NO effect. The
# result is constant log spam in the RouterOS container log (filter
# "Accept: TCP" verdicts, "netcheck: [v1] report", "wg: [v2]" handshakes and
# keepalives) — see tailscale/tailscale#12158 and #1548.
#
# The injected file (build-tagged ts_omit_logtail, so it's a no-op if logtail
# is ever re-enabled) registers a log writer in init() that drops lines
# carrying a [v1]+ tag, restoring the equivalent of logtail's StderrLevel=0
# default. Setting TS_LOG_VERBOSITY=1 (or higher) in the container environment
# disables the filter at runtime for debugging — no rebuild needed.
COPY patches/stderr_verbosity_filter.go cmd/tailscaled/
# Build a minimal combined binary (tailscale CLI + tailscaled daemon in one file). # Build a minimal combined binary (tailscale CLI + tailscaled daemon in one file).
# #
# Tag strategy — ALLOWLIST, not blocklist: # Tag strategy — ALLOWLIST, not blocklist:
@@ -105,59 +75,6 @@ COPY patches/stderr_verbosity_filter.go cmd/tailscaled/
# waiting for completion") WITHOUT printing the auth URL # waiting for completion") WITHOUT printing the auth URL
# or confirming success. Including it makes interactive # or confirming success. Including it makes interactive
# 'up' behave normally (blocks, prints login URL). # 'up' behave normally (blocks, prints login URL).
# netstack — gVisor userspace network stack. Counter-intuitively
# REQUIRED even though the router uses a real kernel TUN
# (NOT --tun=userspace-networking). In v1.98.5 the
# 100.100.100.100:53 MagicDNS listener is served ONLY by
# netstack's handleLocalPackets, installed via
# PreFilterPacketOutboundToWireGuardNetstackIntercept.
# The non-netstack "engine" interceptor that the wrap.go
# comments claim handles quad-100 "if netstack is not
# installed" does NOT actually do so on Linux (its body
# only reflects loopback on darwin/ios/plan9, else
# Accept). So with ts_omit_netstack, NOTHING absorbs
# packets to 100.100.100.100: queries fall through to
# WireGuard, no peer owns that IP, and even tailnet-name
# resolution (and 'ping host.tailnet.ts.net') times out.
# The 'dns' tag links the resolver but nothing routes
# packets to it without netstack — the two tags are
# independent (dns has no Dep on netstack). Omitting
# netstack ALSO triggered a panic("unreachable") in
# net/tstun.invertGSOChecksum on the exit-node inject
# path (HasNetstack=const false made the guard always
# panic); enabling netstack makes that guard dead code,
# fixing the crash as a side effect. Cost (arm64, vs a
# netstack-omitted build): ~+0.5 MB extracted on flash
# and ~+2.3 MB resident RAM after UPX decompression —
# measured, acceptable for a 16 MB-flash router.
# gro — Generic Receive Offload (perf). Depends on netstack;
# pulled in with it. Small, and improves throughput on
# the netstack DNS/inject path.
# peerapiserver — REQUIRED to be a functional exit node. In v1.98.5
# 'advertiseexitnode' DECLARES a dependency on
# peerapiserver (featuretags.go Deps, "to run the ExitDNS
# server"), but this build's allowlist works by stripping
# individual ts_omit_ tags and does NOT re-resolve Deps —
# so featuretags --min still emitted ts_omit_peerapiserver
# and our advertiseexitnode opt-in alone left it omitted.
# peerapiserver gates the entire PeerAPI HTTP server,
# including the /dns-query DoH endpoint (peerapi.go,
# guarded by buildfeatures.HasPeerAPIServer). Without it
# initPeerAPIListenerLocked() returns early: the node
# never advertises the PeerAPIDNS service, so exit-node
# CLIENTS' exitNodeCanProxyDNS(thisNode) returns false.
# With no tailnet global nameserver configured, the
# client's resolver then has an empty Routes["."] and
# returns an INSTANT authoritative SERVFAIL locally
# (forwarder.go servfailResponse, aa=1, 0 ms, no I/O) —
# i.e. devices using this router as their exit node could
# not resolve PUBLIC names. Including peerapiserver makes
# the node serve the exit-node DoH DNS proxy, so clients
# get public DNS automatically (the normal exit-node
# behavior) with no tailnet DNS config required.
# peerapiserver has NO Deps and pulls in no large
# subsystems — a small addition. (outboundproxy is NOT
# needed for this and stays omitted.)
# #
# Everything else remains omitted, including (rationale): # Everything else remains omitted, including (rationale):
# clientupdate — DELIBERATELY removed. The built-in updater would download # clientupdate — DELIBERATELY removed. The built-in updater would download
@@ -182,11 +99,9 @@ COPY patches/stderr_verbosity_filter.go cmd/tailscaled/
# which is exactly the flash wear we want to avoid. # which is exactly the flash wear we want to avoid.
# logtail — no persistent log writes to flash; also pass # logtail — no persistent log writes to flash; also pass
# --no-logs-no-support at runtime # --no-logs-no-support at runtime
# netstack+gro — userspace networking; router uses kernel TUN
# ssh — not needed; access via MikroTik SSH + tailscale CLI # ssh — not needed; access via MikroTik SSH + tailscale CLI
# all GUI/desktop/cloud/k8s features — irrelevant for a headless router # all GUI/desktop/cloud/k8s features — irrelevant for a headless router
#
# NOTE: netstack/gro are NOT in this omit list — see the opted-in section above
# for why MagicDNS quad-100 serving structurally requires them in v1.98.5.
RUN mkdir -p /out && \ RUN mkdir -p /out && \
ALL_OMIT=$(GOOS= GOARCH= go run ./cmd/featuretags --min --add=osrouter) && \ ALL_OMIT=$(GOOS= GOARCH= go run ./cmd/featuretags --min --add=osrouter) && \
@@ -203,9 +118,6 @@ RUN mkdir -p /out && \
-e 's/ts_omit_iptables,\{0,1\}//g' \ -e 's/ts_omit_iptables,\{0,1\}//g' \
-e 's/ts_omit_unixsocketidentity,\{0,1\}//g' \ -e 's/ts_omit_unixsocketidentity,\{0,1\}//g' \
-e 's/ts_omit_ipnbus,\{0,1\}//g' \ -e 's/ts_omit_ipnbus,\{0,1\}//g' \
-e 's/ts_omit_netstack,\{0,1\}//g' \
-e 's/ts_omit_gro,\{0,1\}//g' \
-e 's/ts_omit_peerapiserver,\{0,1\}//g' \
-e 's/,$//' \ -e 's/,$//' \
) && \ ) && \
echo "Build tags: ${TAGS}" && \ echo "Build tags: ${TAGS}" && \
@@ -264,7 +176,7 @@ RUN printf '%s\n' \
chmod +x /out/usrlocalbin/entrypoint.sh chmod +x /out/usrlocalbin/entrypoint.sh
# ============================================================================= # =============================================================================
# Stage 2: Custom minimal busybox (cross-compiled, runs natively on build host) # Stage 2: Custom minimal busybox
# ============================================================================= # =============================================================================
# The official busybox:musl image ships all ~404 applets at ~1.24 MB. For a # 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 # debug shell on a flash-constrained router we only need ~100 applets, so we
@@ -281,56 +193,15 @@ RUN printf '%s\n' \
# acceptable for an occasional debug shell. RouterOS stores the EXTRACTED # 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. # rootfs on disk (overlayfs), so the ~190 kB UPX saving is real on-disk space.
# #
# This stage runs NATIVELY on the build host (--platform=$BUILDPLATFORM) and # This stage runs on the TARGET platform (no --platform override): gcc then
# cross-compiles busybox for the target architecture using clang+lld via the # produces native target-arch binaries directly. Under buildx this is
# tonistiigi/xx helpers. This eliminates QEMU emulation from the busybox build, # transparently emulated via binfmt/QEMU for non-native targets.
# which was the main source of slowness for arm64/arm/v7 targets. FROM alpine:3.24.0@sha256:8ddefa941e689fc29abcdeb8dae3b3c6d139cc08ce9a52633931160701770685 AS busybox
#
# Cross-compilation setup:
# - xx-apk installs musl-dev and linux-headers for the TARGET arch under
# /<triple> (a secondary sysroot), while clang/lld/upx/make stay native.
# - xx-clang --setup-target-triple creates <triple>-clang / <triple>-cc
# aliases in PATH that busybox's Makefile picks up via CROSS_COMPILE.
# - Busybox make receives:
# CROSS_COMPILE=<triple>- → picks up <triple>-clang (from xx aliases)
# CC=clang → use clang (aliased target via CROSS_COMPILE)
# HOSTCC=gcc → compile host helper tools with native gcc
# - upx (native x86_64 binary) can compress target-arch binaries since UPX
# operates on the ELF file format regardless of the target ISA.
#
# Applet symlink probing: for native-arch builds the probe runs directly;
# for cross-compiled binaries we use QEMU user-mode emulation (from binfmt)
# only for this one lightweight probe step (busybox --help per applet), not
# for the compile itself. The probe can alternatively be skipped by using
# a pre-enumerated applet list, but the current approach is simpler.
FROM --platform=$BUILDPLATFORM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS busybox
# Copy xx cross-compilation helpers (xx-clang, xx-apk, xx-info, etc.)
COPY --from=xx / /
# renovate: datasource=docker depName=busybox versioning=docker # renovate: datasource=docker depName=busybox versioning=docker
ARG BUSYBOX_VERSION=1.38.0 ARG BUSYBOX_VERSION=1.37.0
# Target platform ARGs (provided automatically by buildx). RUN apk add --no-cache build-base linux-headers wget bzip2 perl upx
ARG TARGETPLATFORM
ARG TARGETARCH
ARG TARGETVARIANT
# Native build tools (clang/lld for cross-compiling; gcc/make/upx run natively).
# xx-apk installs the target-arch sysroot: musl-dev (C library headers + CRT),
# gcc (provides crtbeginS.o/crtendS.o and libgcc needed by clang on Alpine),
# and linux-headers (required by busybox for <linux/*.h> / <net/*.h>).
RUN apk add --no-cache \
clang \
lld \
llvm \
gcc \
make \
wget \
bzip2 \
perl \
upx && \
xx-apk add --no-cache musl-dev gcc linux-headers
RUN wget -q https://busybox.net/downloads/busybox-${BUSYBOX_VERSION}.tar.bz2 \ RUN wget -q https://busybox.net/downloads/busybox-${BUSYBOX_VERSION}.tar.bz2 \
&& tar xf busybox-${BUSYBOX_VERSION}.tar.bz2 && tar xf busybox-${BUSYBOX_VERSION}.tar.bz2
@@ -338,34 +209,7 @@ WORKDIR /busybox-${BUSYBOX_VERSION}
# allnoconfig = every feature OFF; then enable only the curated applet set. # allnoconfig = every feature OFF; then enable only the curated applet set.
COPY busybox-applets.config /tmp/applets.config COPY busybox-applets.config /tmp/applets.config
# Set up xx cross-compiler aliases (<triple>-clang, <triple>-cc, etc.) and RUN make allnoconfig && \
# build busybox.
#
# Key make variables:
# ARCH — busybox ARCH; must match the cross-target, not the build
# host. busybox's Makefile would otherwise read SUBARCH from
# `uname -m` (the BUILD host's arch) which is wrong when
# cross-compiling. We map TARGETARCH to busybox's arch name.
# busybox uses -include arch/$(ARCH)/Makefile; missing arch
# dirs are silently ignored, so any value is safe.
# CC — busybox defaults to $(CROSS_COMPILE)gcc. We override CC to
# the full <triple>-clang path so it resolves to the xx alias
# (which sets --target and --sysroot for the cross-compiler).
# Setting CC= avoids needing a <triple>-gcc symlink.
# HOSTCC — native compiler for host-side build tools (scripts/kconfig,
# gen_build_files, etc.); must NOT be the cross-compiler.
# SKIP_STRIP — defer stripping to after symlink probing (we strip below
# with llvm-strip, which handles any target ELF arch).
RUN xx-clang --setup-target-triple && \
CROSS=$(xx-info triple) && \
# Map TARGETARCH to the busybox ARCH value.
case "${TARGETARCH}" in \
amd64) BUSYBOX_ARCH=x86_64 ;; \
arm64) BUSYBOX_ARCH=aarch64 ;; \
arm) BUSYBOX_ARCH=arm ;; \
*) BUSYBOX_ARCH=${TARGETARCH} ;; \
esac && \
make allnoconfig ARCH="${BUSYBOX_ARCH}" && \
while read -r sym; do \ while read -r sym; do \
case "$sym" in ''|\#*) continue ;; esac; \ case "$sym" in ''|\#*) continue ;; esac; \
if grep -q "^# CONFIG_${sym} is not set" .config; then \ if grep -q "^# CONFIG_${sym} is not set" .config; then \
@@ -374,15 +218,9 @@ RUN xx-clang --setup-target-triple && \
echo "CONFIG_${sym}=y" >> .config; \ echo "CONFIG_${sym}=y" >> .config; \
fi; \ fi; \
done < /tmp/applets.config && \ done < /tmp/applets.config && \
yes "" | make oldconfig ARCH="${BUSYBOX_ARCH}" >/dev/null 2>&1 && \ yes "" | make oldconfig >/dev/null 2>&1 && \
make -j"$(nproc)" \ make -j"$(nproc)" >/dev/null 2>&1 && \
ARCH="${BUSYBOX_ARCH}" \ strip busybox
CROSS_COMPILE="${CROSS}-" \
CC="${CROSS}-clang" \
HOSTCC=gcc \
SKIP_STRIP=y \
>/dev/null 2>&1 && \
llvm-strip busybox
# Lay out a minimal rootfs with busybox + an applet symlink per applet. # 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 # Symlinks (argv[0] dispatch) are how busybox selects an applet and make the
@@ -392,10 +230,6 @@ RUN xx-clang --setup-target-triple && \
# for non-applet symbols like FEATURE_* / STATIC, which we filter out). # for non-applet symbols like FEATURE_* / STATIC, which we filter out).
# We generate symlinks from the UNCOMPRESSED binary (so the probe is reliable), # We generate symlinks from the UNCOMPRESSED binary (so the probe is reliable),
# then UPX-compress the binary in place afterwards. # then UPX-compress the binary in place afterwards.
#
# Note: probing cross-compiled binaries requires binfmt/QEMU user-mode. This
# is only a lightweight per-applet help-flag check, not a full emulated build.
# If QEMU is unavailable in CI, replace the probe with a static applet list.
RUN mkdir -p /rootfs/bin && \ RUN mkdir -p /rootfs/bin && \
grep '^CONFIG_.*=y' .config \ grep '^CONFIG_.*=y' .config \
| sed -e 's/^CONFIG_//' -e 's/=y$//' \ | sed -e 's/^CONFIG_//' -e 's/=y$//' \
-1
View File
@@ -70,7 +70,6 @@ ARMv5 (hEX Refresh / hAP ax S) is **not** supported — see
|---|---| |---|---|
| `Dockerfile` | Multi-stage, multi-arch build (cross-compiled Go + custom busybox) | | `Dockerfile` | Multi-stage, multi-arch build (cross-compiled Go + custom busybox) |
| `busybox-applets.config` | Curated busybox applet set | | `busybox-applets.config` | Curated busybox applet set |
| `patches/` | Source files injected into the Tailscale tree at build time (stderr verbosity filter) |
| `build.sh` | Build all/one arch, optionally export per-arch tarballs | | `build.sh` | Build all/one arch, optionally export per-arch tarballs |
| `routeros/update-tailscale.rsc` | RouterOS auto-update script (digest compare + recreate) | | `routeros/update-tailscale.rsc` | RouterOS auto-update script (digest compare + recreate) |
| `.woodpecker/` | CI: Renovate cron, release tagging, multi-arch publish | | `.woodpecker/` | CI: Renovate cron, release tagging, multi-arch publish |
+1 -2
View File
@@ -12,8 +12,7 @@
# #
# Requirements: # Requirements:
# - docker with buildx # - docker with buildx
# - For non-native targets: binfmt/QEMU emulators registered for the applet # - For non-native targets: binfmt/QEMU emulators registered, e.g.:
# symlink probe step (a minor step; the full C/Go compile is native):
# docker run --privileged --rm tonistiigi/binfmt --install arm64,arm # docker run --privileged --rm tonistiigi/binfmt --install arm64,arm
set -eu set -eu
+16 -237
View File
@@ -15,26 +15,22 @@ Measured flattened rootfs for the arm64 image:
| Component | On-disk size | | Component | On-disk size |
|---|---| |---|---|
| `tailscale.combined` (UPX-compressed) | ~3.47 MB | | `tailscale.combined` (UPX-compressed) | ~2.98 MB |
| custom static busybox (UPX, ~100 applets) | ~218 kB | | custom static busybox (UPX, ~100 applets) | ~218 kB |
| CA certificates | ~213 kB | | CA certificates | ~213 kB |
| **Total extracted rootfs** | **~3.9 MB** | | **Total extracted rootfs** | **~3.4 MB** |
The `tailscale.combined` figure includes `netstack` (gVisor), which adds (The compressed image / transfer tarball is ~3.34.3 MB depending on arch.)
~0.5 MB on disk over a netstack-omitted build — a deliberate inclusion, see
[Why netstack is required (even with a kernel TUN)](#why-netstack-is-required-even-with-a-kernel-tun).
(The compressed image / transfer tarball is ~3.84.3 MB depending on arch.)
| Arch | Image (compressed) | | Arch | Image (compressed) |
|---|---| |---|---|
| amd64 | ~4.3 MB | | amd64 | ~4.2 MB |
| arm64 | ~4.0 MB | | arm64 | ~3.5 MB |
| arm/v7 | ~4.0 MB | | arm/v7 | ~3.5 MB |
On a deployed RouterOS device the container consumes **~4.2 MiB of flash** On a deployed RouterOS device the container consumes **~3.7 MiB of flash**
(measured by `free-hdd-space` delta). Note that `du` *inside* the container (measured by `free-hdd-space` delta). Note that `du` *inside* the container
reports roughly double that (~8 MB) — that is RouterOS block-allocation reports roughly double that (~7 MB) — that is RouterOS block-allocation
rounding, **not** real usage or duplication; see rounding, **not** real usage or duplication; see
[Avoiding overlayfs layer duplication](#avoiding-overlayfs-layer-duplication) [Avoiding overlayfs layer duplication](#avoiding-overlayfs-layer-duplication)
for how to measure correctly. for how to measure correctly.
@@ -122,13 +118,13 @@ delta**, not `du`:
/system/resource/print # note free-hdd-space before and after adding the container /system/resource/print # note free-hdd-space before and after adding the container
``` ```
The container should consume **~4.2 MiB** of flash (e.g. 94.6 → 90.4 MiB free). The container should consume **~3.7 MiB** of flash (e.g. 94.6 → 90.9 MiB free).
Do **not** trust `du` inside the container for this. Busybox `du` reports Do **not** trust `du` inside the container for this. Busybox `du` reports
*allocated blocks*, and RouterOS's container store rounds the ~3.5 MB binary up *allocated blocks*, and RouterOS's container store rounds a ~3 MB file up to
to ~7 MB of blocks — so `du -sx /` reports ~8 MB even though real flash use is ~6 MB of blocks — so `du -sx /` reports ~7 MB even though real flash use is
~4.2 MB. `ls -la /usr/local/bin` confirms the binary's true content size ~3.7 MB. `ls -la /usr/local/bin` confirms the binary's true content size
(~3.5 MB) and that it is a single file with two symlinks (no duplication). (~3.1 MB) and that it is a single file with two symlinks (no duplication).
The image itself carries the binary in exactly one layer (verified at the blob The image itself carries the binary in exactly one layer (verified at the blob
level); the inflation is purely the filesystem's block accounting. level); the inflation is purely the filesystem's block accounting.
@@ -153,9 +149,7 @@ that's a separate build, not just a `--platform` change.
| `advertise-routes` | Expose LAN subnets to the tailnet | | `advertise-routes` | Expose LAN subnets to the tailnet |
| `use-exit-node` | Route the router's own traffic via a remote exit node | | `use-exit-node` | Route the router's own traffic via a remote exit node |
| `accept-routes` | Receive subnet routes from other tailnet nodes | | `accept-routes` | Receive subnet routes from other tailnet nodes |
| DNS / MagicDNS | Resolve `*.ts.net` names (resolver + resolv.conf manager). **Note:** serving `100.100.100.100` also requires `netstack` — see [Why netstack is required (even with a kernel TUN)](#why-netstack-is-required-even-with-a-kernel-tun) | | DNS / MagicDNS | Resolve `*.ts.net` names |
| `netstack` + `gro` | gVisor userspace stack. Counter-intuitively **required** to serve MagicDNS on `100.100.100.100`, even though the router uses a real kernel TUN — see [Why netstack is required (even with a kernel TUN)](#why-netstack-is-required-even-with-a-kernel-tun) |
| `peerapiserver` | Serves the PeerAPI, including the `/dns-query` DoH endpoint that lets **exit-node clients resolve public DNS automatically**. A declared dependency of `advertise-exit-node` that the allowlist didn't pull in — see [Why peerapiserver is required for exit-node DNS](#why-peerapiserver-is-required-for-exit-node-dns) |
| portmapper (NAT-PMP/PCP/UPnP) | Punch through upstream NAT | | portmapper (NAT-PMP/PCP/UPnP) | Punch through upstream NAT |
| listenrawdisco | Raw socket disco for better NAT traversal | | listenrawdisco | Raw socket disco for better NAT traversal |
| health | Powers `tailscale status` output | | health | Powers `tailscale status` output |
@@ -170,8 +164,9 @@ that's a separate build, not just a `--platform` change.
|---|---| |---|---|
| `clientupdate` | **Deliberately removed** — see [Why the built-in updater is removed](#why-the-built-in-updater-is-removed) | | `clientupdate` | **Deliberately removed** — see [Why the built-in updater is removed](#why-the-built-in-updater-is-removed) |
| `cachenetmap` | **Deliberately removed** — see [Why netmap disk-caching is removed](#why-netmap-disk-caching-is-removed) | | `cachenetmap` | **Deliberately removed** — see [Why netmap disk-caching is removed](#why-netmap-disk-caching-is-removed) |
| `logtail` | Would attempt persistent log writes; wear flash. Removing it also removes stderr verbosity filtering — restored by an injected filter, see [Log verbosity filtering](#log-verbosity-filtering) | | `logtail` | Would attempt persistent log writes; wear flash |
| `netlog` | Network flow logging; separate concern | | `netlog` | Network flow logging; separate concern |
| `netstack` + `gro` | Userspace/gVisor networking; router uses kernel TUN |
| `ssh` | Access via MikroTik SSH + `tailscale` CLI instead | | `ssh` | Access via MikroTik SSH + `tailscale` CLI instead |
| `linuxdnsfight` | inotify on `/etc/resolv.conf`; no systemd in container | | `linuxdnsfight` | inotify on `/etc/resolv.conf`; no systemd in container |
| `networkmanager` / `resolved` / `dbus` / `sdnotify` | No systemd stack in container | | `networkmanager` / `resolved` / `dbus` / `sdnotify` | No systemd stack in container |
@@ -231,206 +226,6 @@ the in-memory resilience (the common case) while eliminating per-netmap flash
writes. Only `tailscaled.state` (written on auth / key rotation) ever touches writes. Only `tailscaled.state` (written on auth / key rotation) ever touches
flash. flash.
### Why netstack is required (even with a kernel TUN)
This is the least obvious inclusion in the build, so it is documented in full.
`netstack` is Tailscale's embedded **gVisor userspace TCP/IP stack**. The
natural assumption — and what earlier versions of this build acted on — is that
a router which owns a **real kernel TUN device** (it is *not* run with
`--tun=userspace-networking`) has no use for a userspace stack, so `netstack`
(and its dependent `gro`) can be omitted to save space. That assumption is
**wrong for one specific, important path: MagicDNS.**
**MagicDNS on `100.100.100.100` is served only by netstack.** In Tailscale
v1.98.5 the in-process listener for the Tailscale service IP
(`100.100.100.100:53`, UDP) is installed exclusively by netstack's
`handleLocalPackets`, wired into the TUN wrapper as
`PreFilterPacketOutboundToWireGuardNetstackIntercept`
(`wgengine/netstack/netstack.go`). When a packet leaves the host toward
`100.100.100.100`, this hook absorbs it into the gVisor stack, whose UDP-53
acceptor runs the MagicDNS resolver.
**The "engine fallback" does not actually exist.** The TUN wrapper consults a
second hook, `PreFilterPacketOutboundToWireGuardEngineIntercept`, and a comment
in `net/tstun/wrap.go` claims it "primarily handles quad-100 if netstack is not
installed." In v1.98.5 that comment is **false on Linux**: the engine
`handleLocalPackets` (`wgengine/userspace.go`) only reflects loopback on
darwin/ios/plan9 and otherwise returns `Accept` — it never touches
`100.100.100.100`. So with `ts_omit_netstack` there is **no** code that absorbs
quad-100 packets at all.
**`dns` and `netstack` are independent tags.** The `dns` feature (which this
build opts in) links the resolver and the `/etc/resolv.conf` manager, but it has
no dependency on `netstack` and does **not** install any quad-100 transport.
The net result of `dns` on + `netstack` off is a resolver that is correctly
wired up but that **never receives any packets** — the worst kind of silent
breakage. Symptoms observed on the device:
- `/etc/resolv.conf` correctly points at `100.100.100.100` (the manager works),
- but `dig anything @100.100.100.100` from inside the container **times out**
("no servers could be reached"),
- and even tailnet-internal names fail: `ping host.<tailnet>.ts.net`
`bad address` (a name that needs **no** upstream forwarding still can't
resolve, proving the listener itself is dead, not an upstream-resolver issue),
- while `ping 1.1.1.1` (a raw IP needing no DNS) works fine over the kernel data
path — confirming forwarding/exit-node connectivity is unaffected and isolating
the fault to DNS serving.
**It also fixed a crash.** Omitting `netstack` set `buildfeatures.HasNetstack`
to a compile-time `false`, which turned the guard in
`net/tstun.invertGSOChecksum` (`if !HasNetstack { panic("unreachable") }`) into
an always-panic. That function is called on the packet-injection path used when
enabling exit-node mode, producing `panic: unreachable` and a daemon restart
loop. Enabling `netstack` makes `HasNetstack` a const `true`, so the guard
becomes dead code and the crash disappears as a side effect — fixed at the root
cause rather than patched around.
**Cost.** Measured on arm64, a netstack-enabled build versus a netstack-omitted
one:
| Metric | netstack omitted | netstack enabled | Delta |
|---|---|---|---|
| Extracted rootfs (flash) | ~3.42 MB | ~3.91 MB | **+0.49 MB** |
| `tailscale.combined` on disk (UPX) | ~2.99 MB | ~3.47 MB | +0.48 MB |
| Resident RAM after UPX decompress | ~12.25 MB | ~14.56 MB | **+2.31 MB** |
The flash cost (~0.5 MB) is negligible on a 16 MB-class device. The RAM cost
(~2.3 MB resident) is the real consideration on low-memory models, but is
acceptable given that without it MagicDNS is entirely non-functional. The
trade is: **half a megabyte of flash to make MagicDNS work at all.** `gro`
(Generic Receive Offload) depends on `netstack` and is pulled in alongside it;
it is small and improves throughput on the netstack path.
**Caveat for future Tailscale bumps.** This coupling (quad-100 serving living
only in netstack) is an upstream implementation detail, not a stable contract.
If a future release adds a genuine non-netstack quad-100 path — or the daemon
itself is refactored — re-test whether `netstack` can be dropped again. The
canary is simple: from inside the container, `dig google.com @100.100.100.100`
must return answers and `ping <host>.<tailnet>.ts.net` must resolve.
### Why peerapiserver is required for exit-node DNS
This is a second non-obvious DNS inclusion, and it exposes a limitation of the
allowlist build strategy.
**Symptom.** With `netstack` enabled, MagicDNS worked from the router and from
LAN hosts, including public names. But a device using this router **as its exit
node** could not resolve public names: `dig google.com @100.100.100.100` on the
*client* returned an instant authoritative `SERVFAIL` (`flags: qr aa rd ad`,
`Query time: 0 msec`, "recursion not available"). Tailnet names and raw-IP
connectivity (e.g. `ping 1.1.1.1`) through the exit node worked.
**Root cause.** The `SERVFAIL` is generated **on the client**, locally, with no
network I/O — which is why it is instant and authoritative. The path
(traced through v1.98.5 source):
1. The client's query for `google.com` reaches its in-process resolver, which
determines the name is not a tailnet name and marks it for forwarding
(`net/dns/resolver/tsdns.go`).
2. The forwarder looks up which upstream resolver to use for the catch-all
`"."` route (`net/dns/resolver/forwarder.go``resolvers()`).
3. That route set is **empty**, so `forwardWithDestChan` short-circuits and
synthesises an authoritative `SERVFAIL` (`servfailResponse`, `aa=1`) without
opening any socket. The query never reaches this router at all.
Why the route set is empty: when a client selects an exit node,
`dnsConfigForNetmap` (`ipn/ipnlocal/node_backend.go`) deliberately routes **all**
default DNS through the exit node and drops the client's own LAN/system
resolver — the whole premise of an exit node is "send everything, including
DNS, through me." It does this by setting the client's default resolver to the
exit node's **DoH proxy** URL (`http://<peer>/dns-query`). But that only happens
if `exitNodeCanProxyDNS(thisRouter)` returns true — i.e. if **this router
advertises a working PeerAPI DoH endpoint**. If it does not, and there is no
tailnet global nameserver to fall back to, the client ends up with an empty
default route and returns `SERVFAIL`.
**Why this router didn't advertise the DoH proxy.** The `/dns-query` DoH
endpoint is part of the **PeerAPI server**, gated by
`buildfeatures.HasPeerAPIServer` (`ipn/ipnlocal/peerapi.go`). With
`ts_omit_peerapiserver`, `initPeerAPIListenerLocked()` returns early: no PeerAPI
listener is created, the `PeerAPIDNS` service is never advertised, and
`peerCanProxyDNS()` is false for this node on every client.
**The allowlist gap that caused it.** In `feature/featuretags/featuretags.go`,
`advertiseexitnode` **declares a dependency on `peerapiserver`** ("to run the
ExitDNS server"). Upstream's own `--add` resolution would have pulled it in.
But this build's allowlist works differently: it runs `featuretags --min` to get
the full omit set, then strips the specific `ts_omit_<feature>` tags it wants —
it does **not** re-resolve transitive `Deps`. So opting in `advertiseexitnode`
did not pull in `peerapiserver`, and `featuretags --min` had emitted
`ts_omit_peerapiserver`, leaving the node an exit node *without* its declared
ExitDNS dependency — a feature combination upstream's graph says shouldn't
occur. Including `peerapiserver` explicitly closes the gap.
> **Known limitation:** the allowlist (strip-individual-`ts_omit_`-tags) does
> not resolve feature dependencies. When opting a feature in, check its `Deps`
> in `featuretags.go` and add them explicitly. `peerapiserver` is the only such
> gap found and fixed so far; a full dependency audit has not been done.
**Cost.** Negligible. `peerapiserver` has **no** `Deps` and pulls in no large
subsystems; measured at ~+10 kB on the UPX'd binary (arm64), rootfs unchanged
within measurement noise.
**Result.** The router now serves the exit-node DoH DNS proxy, so devices using
it as their exit node resolve public names automatically — the normal exit-node
behavior — with **no** tailnet DNS configuration required. (Setting a tailnet
global nameserver in the admin console is an alternative runtime fix that also
works, by populating the client's default resolver directly; it is not required
once the router serves the proxy.)
**Canary for future bumps:** from a client using this router as exit node,
`dig google.com @100.100.100.100` must return real answers with `flags: ... ra`
(recursion available) and a non-zero query time.
### Log verbosity filtering
Upstream `tailscaled` embeds verbosity tags (`[v1]`, `[v2]`, …) inside its log
messages and relies on the **logtail** subsystem to act on them: in a stock
build, logtail's log policy intercepts everything written via the standard
`log` package, parses the tag, and only writes a line to stderr when its level
is within `--verbose` (default 0 — non-verbose messages only). The `--verbose`
flag is literally wired into logtail (`pol.SetVerbosityLevel(args.verbose)` in
`cmd/tailscaled/tailscaled.go`).
This build omits logtail (`ts_omit_logtail`) to avoid log-upload code and
flash writes — but that removed the stderr filtering along with it, as
collateral damage. The result: every verbose line went **unfiltered** to
stderr and into the RouterOS container log, with the literal `[v1]` tag still
in the text. On an active node that means constant spam, several lines per
minute:
```
tailscale: ... [v1] Accept: TCP{...:53256 > ...:50000} 391 tcp non-syn
tailscale: ... netcheck: [v1] report: udp=true v6=true ... derp=22 ...
tailscale: ... wg: [v2] [0GwzF] - Receiving keepalive packet
```
This is a [known](https://github.com/tailscale/tailscale/issues/12158)
[long-standing](https://github.com/tailscale/tailscale/issues/1548) complaint
even in full builds, and RouterOS logging offers no way to discard matching
messages (no drop action, rules are all-match — a regex rule duplicates rather
than diverts).
The fix here: the build injects a ~20-line Go file
(`patches/stderr_verbosity_filter.go`, copied into `cmd/tailscaled/` before
`go build`) whose `init()` wraps the standard log output and silently drops
any line carrying a `[v1]`/`[v2]`/`[v3]` tag. This restores the exact
equivalent of logtail's default `StderrLevel=0` behavior without pulling in
the upload machinery. Properties:
- **No upstream sources modified** — it's a new file in the package, so it
survives Tailscale version bumps without rebasing (only relies on the
daemon using the stdlib `log` package, which is core behavior).
- **Build-tagged `//go:build ts_omit_logtail`** — if logtail is ever
re-enabled, the file compiles out automatically and logtail's own filtering
takes over; the two can never conflict.
- **Runtime escape hatch** — setting the `TS_LOG_VERBOSITY=1` environment
variable disables the filter (and, conveniently, the same knob is read by
upstream as the default `--verbose` level). Verbose logs are one
`/container/envs/add` away; no rebuild needed. See
[USAGE.md → Logging](USAGE.md#logging).
## Volume layout ## Volume layout
Two mount points, with different persistence requirements: Two mount points, with different persistence requirements:
@@ -451,22 +246,6 @@ Only the small, rarely-written state file touches flash; the socket dir is
tmpfs. The netmap is held in memory only — see tmpfs. The netmap is held in memory only — see
[Why netmap disk-caching is removed](#why-netmap-disk-caching-is-removed). [Why netmap disk-caching is removed](#why-netmap-disk-caching-is-removed).
### What lives in the state dir
| File | Purpose | Write frequency |
|---|---|---|
| `tailscaled.state` | Node identity, auth keys, prefs | On auth / key rotation / prefs change |
| `derpmap.cached.json` | Cached DERP relay server list for **bootstrap DNS**: at cold start with broken/unavailable DNS, tailscaled asks DERP servers to resolve the control plane. The binary ships a static DERP list, but it goes stale; this cache keeps the current one. | Once at first auth, then **only when Tailscale's relay infrastructure changes** (a few times a year). `dnsfallback.UpdateCache` has a deep-equal guard and skips the write when the DERP map is unchanged — netmap churn never touches it. |
`derpmap.cached.json` is intentionally **kept** despite the flash-wear policy:
the policy targets *frequent* writes (netmap deltas, logs), not one-shot
caches. On a router this cache is genuinely useful — after a power outage the
device may boot with WAN up but upstream DNS broken, exactly the case where a
fresh DERP list lets the node reach the control plane anyway. With
`cachenetmap` omitted, this file and `tailscaled.state` are the only cold-start
resilience the node has. (There is no `ts_omit_*` tag for it; it is written
only because `--statedir` is set.)
## Flash wear protection ## Flash wear protection
Several measures are in place to avoid wearing out internal flash: Several measures are in place to avoid wearing out internal flash:
+2 -40
View File
@@ -9,7 +9,7 @@ reasoning behind these choices, see [DESIGN.md](DESIGN.md).
## Deploy on MikroTik (RouterOS) ## Deploy on MikroTik (RouterOS)
Verified on RouterOS 7.23 (arm64, CRS418). Commands are grouped into Verified on RouterOS 7.21.2 (arm64, CRS418). Commands are grouped into
copy-paste blocks, defaults should fit most configurations. copy-paste blocks, defaults should fit most configurations.
> Because the image has no built-in updater (the `clientupdate` feature is > Because the image has no built-in updater (the `clientupdate` feature is
@@ -19,9 +19,7 @@ copy-paste blocks, defaults should fit most configurations.
### 0. Prerequisites ### 0. Prerequisites
- RouterOS >= 7.23 with the **container** package installed - RouterOS >7.13 with the **container** package installed.
(7.23 is needed for container `restart-policy`; the deploy itself works on
>= 7.13 if you drop the restart options).
- Container mode enabled ([documentation](https://manual.mikrotik.com/docs/System%20Information%20and%20Utilities/device-mode/#changing-mode-of-device-mode)): - Container mode enabled ([documentation](https://manual.mikrotik.com/docs/System%20Information%20and%20Utilities/device-mode/#changing-mode-of-device-mode)):
``` ```
@@ -82,8 +80,6 @@ just that directory:
mountlists=tailscale_state \ mountlists=tailscale_state \
logging=yes \ logging=yes \
start-on-boot=yes \ start-on-boot=yes \
restart-policy=on-failure \
restart-interval=10s \
name=tailscale name=tailscale
``` ```
@@ -181,40 +177,6 @@ When this is configured, you can connect to other tailscale machines using
`[device name].[tailnet name].ts.net`. You can see and change assigned `[device name].[tailnet name].ts.net`. You can see and change assigned
Tailnet DNS name in Tailscale admin panel under DNS tab. Tailnet DNS name in Tailscale admin panel under DNS tab.
## Logging
The container logs to the RouterOS log (topic `container`) via `logging=yes`.
Upstream `tailscaled` is notoriously chatty: by default it would emit a line
for every accepted connection (`Accept: TCP{...}`), every netcheck report, and
every WireGuard handshake/keepalive — several lines per minute on an active
node ([tailscale#12158](https://github.com/tailscale/tailscale/issues/12158)).
This image filters those verbose (`[v1]`/`[v2]`-tagged) messages out at the
source, so only meaningful messages (startup, auth, route changes, warnings,
errors) reach the RouterOS log. See
[DESIGN.md → Log verbosity filtering](DESIGN.md#log-verbosity-filtering) for
how and why.
To temporarily get the verbose logs back for debugging (e.g. NAT-traversal
issues), set the `TS_LOG_VERBOSITY` environment variable and recreate the
container with the envlist attached:
```
/container/envs/add list=tailscale_envs name=TS_LOG_VERBOSITY value=1
/container/set [find where name=tailscale] envlist=tailscale_envs
/container/stop [find where name=tailscale]
/container/start [find where name=tailscale]
```
Any value ≥ 1 disables the filter (and raises the daemon's own verbosity by
the same amount). Remove the variable and restart to silence it again:
```
/container/envs/remove [find where name=TS_LOG_VERBOSITY]
/container/stop [find where name=tailscale]
/container/start [find where name=tailscale]
```
## Updating ## Updating
You don't normally do anything: when a new release is published, the You don't normally do anything: when a new release is published, the
-45
View File
@@ -1,45 +0,0 @@
// Copyright (c) mikrotik-tailscale build. Injected at image build time.
// SPDX-License-Identifier: BSD-3-Clause
//go:build ts_omit_logtail
package main
// When logtail is compiled out (ts_omit_logtail), logpolicy is never
// installed (see run() in tailscaled.go: `if buildfeatures.HasLogTail`),
// so log.Printf output goes raw to stderr. Nothing parses the [v1]/[v2]
// verbosity tags Tailscale embeds in log messages, which means every
// verbose line (filter "Accept: TCP", "netcheck: [v1] report",
// "wg: [v2]" handshakes/keepalives) is printed regardless of --verbose.
//
// This restores the equivalent of logtail's StderrLevel=0 behavior:
// drop lines carrying a [v1]+ tag, unless TS_LOG_VERBOSITY is set to
// 1 or higher (runtime escape hatch for debugging — no rebuild needed).
import (
"bytes"
"log"
"os"
)
var verboseLogTags = [][]byte{[]byte("[v1] "), []byte("[v2] "), []byte("[v3] ")}
type stderrVerbosityFilter struct{ w *os.File }
func (f stderrVerbosityFilter) Write(p []byte) (int, error) {
for _, tag := range verboseLogTags {
if bytes.Contains(p, tag) {
// Claim success so the log package doesn't complain;
// the line is intentionally discarded.
return len(p), nil
}
}
return f.w.Write(p)
}
func init() {
if v := os.Getenv("TS_LOG_VERBOSITY"); v != "" && v != "0" {
return
}
log.SetOutput(stderrVerbosityFilter{os.Stderr})
}
+2 -11
View File
@@ -3,9 +3,8 @@
# ============================================================================= # =============================================================================
# Checks the Gitea registry for a new :stable image and, only if the published # Checks the Gitea registry for a new :stable image and, only if the published
# image actually changed, recreates the container. Designed for RouterOS 7.x # image actually changed, recreates the container. Designed for RouterOS 7.x
# (tested target: 7.23, arm64). Requires RouterOS >= 7.23 for the container # (tested target: 7.21.2, arm64). Requires RouterOS >= 7.13 for the :deserialize
# restart-policy properties (and >= 7.13 for the :deserialize command used to # command used to parse the registry token JSON.
# parse the registry token JSON).
# #
# HOW IT DECIDES "something changed": # HOW IT DECIDES "something changed":
# It fetches the manifest digest of the :stable tag from the registry and # It fetches the manifest digest of the :stable tag from the registry and
@@ -60,12 +59,6 @@
:local cInterface "veth-tailscale" :local cInterface "veth-tailscale"
:local cLogging yes :local cLogging yes
:local cStartOnBoot yes :local cStartOnBoot yes
# Restart the container automatically if tailscaled crashes (tailscaled is
# PID 1; if it dies the container stops). on-failure restarts only on abnormal
# exit (a manual /container/stop stays stopped); 10s is a gentle backoff.
# Requires RouterOS >= 7.23.
:local cRestartPolicy "on-failure"
:local cRestartInterval "10s"
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
:log info "$scriptName: checking for image updates" :log info "$scriptName: checking for image updates"
@@ -181,8 +174,6 @@
mountlists=$cMountList \ mountlists=$cMountList \
logging=$cLogging \ logging=$cLogging \
start-on-boot=$cStartOnBoot \ start-on-boot=$cStartOnBoot \
restart-policy=$cRestartPolicy \
restart-interval=$cRestartInterval \
name=$cName name=$cName
} do={ } do={
:log error "$scriptName: container add failed: $e" :log error "$scriptName: container add failed: $e"