From ebf011908ad91f4e88315550208c72536c23b706 Mon Sep 17 00:00:00 2001 From: Lumpiasty Date: Fri, 12 Jun 2026 01:25:44 +0200 Subject: [PATCH] Log verbosity filtering feature --- Dockerfile | 17 ++++++++++ README.md | 1 + docs/DESIGN.md | 50 +++++++++++++++++++++++++++++- docs/USAGE.md | 34 ++++++++++++++++++++ patches/stderr_verbosity_filter.go | 45 +++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 patches/stderr_verbosity_filter.go diff --git a/Dockerfile b/Dockerfile index 2c0fe35..fd7648b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,23 @@ RUN git clone --depth 1 --branch ${TAILSCALE_VERSION} \ 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). # # Tag strategy — ALLOWLIST, not blocklist: diff --git a/README.md b/README.md index 7054d74..8e25322 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ ARMv5 (hEX Refresh / hAP ax S) is **not** supported — see |---|---| | `Dockerfile` | Multi-stage, multi-arch build (cross-compiled Go + custom busybox) | | `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 | | `routeros/update-tailscale.rsc` | RouterOS auto-update script (digest compare + recreate) | | `.woodpecker/` | CI: Renovate cron, release tagging, multi-arch publish | diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 07d4be4..1921797 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -164,7 +164,7 @@ 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) | | `cachenetmap` | **Deliberately removed** — see [Why netmap disk-caching is removed](#why-netmap-disk-caching-is-removed) | -| `logtail` | Would attempt persistent log writes; wear flash | +| `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) | | `netlog` | Network flow logging; separate concern | | `netstack` + `gro` | Userspace/gVisor networking; router uses kernel TUN | | `ssh` | Access via MikroTik SSH + `tailscale` CLI instead | @@ -226,6 +226,54 @@ the in-memory resilience (the common case) while eliminating per-netmap flash writes. Only `tailscaled.state` (written on auth / key rotation) ever touches flash. +### 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 Two mount points, with different persistence requirements: diff --git a/docs/USAGE.md b/docs/USAGE.md index e7524b9..799694c 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -177,6 +177,40 @@ When this is configured, you can connect to other tailscale machines using `[device name].[tailnet name].ts.net`. You can see and change assigned 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 You don't normally do anything: when a new release is published, the diff --git a/patches/stderr_verbosity_filter.go b/patches/stderr_verbosity_filter.go new file mode 100644 index 0000000..632f4a1 --- /dev/null +++ b/patches/stderr_verbosity_filter.go @@ -0,0 +1,45 @@ +// 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}) +}