Skip to content

What the installer refuses

The installer writes to a router it did not configure, over the operator’s own admin ssh session, so there is no privilege boundary between a mistake and the router. What stands in for one is a set of refusals. This page lists them: what install stops on before it writes, what it will not touch, what it treats as a failure, and how uninstall shows that it is done rather than saying so.

install builds the image, prints every command it would run with its exact RouterOS text, and then, in this order:

  1. runs doctor, the read-only preflight, unless --no-doctor is given; a missing prerequisite stops it with N prerequisite(s) missing; nothing was written;
  2. asks write the objects above to the router? [y/N], unless --yes is given; anything but y or Y stops it with not confirmed; nothing written;
  3. only then writes.

plan, and install --dry-run, stop after the listing. The listing masks the token as value="(token)".

upgrade does not get this guarantee. It builds the image, refuses a router where any step of the plan built from its own flags is missing (nothing to upgrade: run install first), and asks the same write the objects above to the router? [y/N] — but it prints no listing and runs no doctor first, so there are no objects above. On y it removes the container, the envlist and the image and writes them again, the envlist from the flags given to upgrade; what –expose opens has what that means for the token.

What install writes to your router

  • a veth
  • one address
  • one interface-list membership
  • one address-list entry
  • an envlist
  • the image tar, unless --remote-image has the router pull the image
  • the container

Every object carries the comment mikroscope:<name> (managed by mikroscope)

mikroscope plan prints every command before anything is written.

uninstall removes by exact tag plus identity, never by pattern, and fails naming the step if anything remains.

Before writing, install asks the router three questions about every step in one ssh connect: is our object there, does something with the same effect exist, and does it carry our tag. An object that exists but does not carry mikroscope’s tag stops install, naming the step:

veth interface veth-mikroscope exists on the router and was not created by mikroscope (no ownership tag); pick another --name/--veth/--subnet, or remove it by hand if it is yours

What counts as “the same effect” is the object’s identity, not its name alone:

Step Collides with any existing Ours when it carries
veth /interface/veth with the same name the tag in comment
router address /ip/address on that veth the tag in comment
interface-list membership member with that interface in that list the tag in comment
address-list membership entry with the /30 in that list the tag in comment
expose dst-nat dstnat rule with that destination address, port and protocol the tag in comment
expose forward accept forward rule with the container’s address, that port and protocol the tag in comment
container a container using the same image file, an envlist named <name>-env, or a file at the image path a container with the tag; an envlist holding the marker

A step that is already ours is skipped, so running install twice creates nothing the second time.

The refusal happens when install reaches the colliding step. Steps before it that were absent have already been created; they carry the tag, and uninstall removes them. uninstall never touches the foreign object.

The envlist and the image carry no comment

Section titled “The envlist and the image carry no comment”

Neither /container/envs nor /file has a comment field, so the container step signs them another way: the first entry written to the envlist is MIKROSCOPE_TAG holding the exact tag, and the image file counts as ours only while that marker exists. A foreign envlist with the same name, a foreign file at the image path, or another container’s envlist is therefore foreign, and stops install. Leftovers of an earlier mikroscope install — an envlist under our marker with no container — are ours to replace, and install clears them before writing new ones.

Every object install creates carries the comment mikroscope:<name> (managed by mikroscope), verbatim. Every network object is removed by that exact comment together with the identity its check used — comment="…", never a pattern match. The container is removed by the comment alone, and the envlist and the image, which carry no comment, by list="<name>-env" and the exact image file name, only while the marker entry holding the exact tag exists. So uninstall cannot reach a hand-made setup, or anything else whose comment or name shares a substring with the container name.

Every find quotes address and port attributes. Unquoted, RouterOS parses them as typed values and the comparison with the stored one comes back empty; verified for both on RB5009UG+S+, RouterOS 7.24.2, .

A RouterOS write prints nothing on success, and over ssh it reports errors as text with exit status 0, abandoning the rest of a ;-joined line. So install treats any output from a write as a failure (create <step>: router said "…") and stops there.

The image tar is uploaded with scp before the container step runs, and before the marker exists. If that step then fails, install removes the uploaded file (undo removed the uploaded …); otherwise it would count as a foreign file on every later attempt.

uninstall reads output the same way in the other direction: a removal that printed something is reported as skip, not gone.

Every flag that reaches a RouterOS command is interpolated into it verbatim. There is no privilege to escalate — the command runs as your admin — but a quote or a semicolon would turn a clear error into a confusing RouterOS syntax error, or a selector into something wider than intended. So every value is checked before the first connect, and one outside these bounds stops the command with the rule it broke:

Flag Accepted
--name ^[A-Za-z0-9][A-Za-z0-9_.-]{0,31}$
--veth, --iface-list, --addr-list ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$
--disk ^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$, or empty for the internal flash; --ephemeral forces tmpfs
--arch ^[a-z0-9]{1,16}$
--token ^[A-Za-z0-9_.-]{0,128}$
--subnet an IPv4 /30, written as its network address
--port 1–65535
--rate 1–100 Hz
--buffer 10–3600 s
--memory-max ^\d{1,6}[KMG]?$
--mem-limit-mb 8–1024
--floor-hz 0–1000
--capture-mb 0–256
--expose needs --lan-address as an IPv4 address, and a non-empty token
--triggers the agent’s own condition list, parsed by agent.ParseTriggers
--remote-image a registry reference: owner/name:1.0.0, with or without a host, no quote, space or semicolon

There is no exception. --triggers is checked by the same gate as the rest: Finish hands it to agent.ParseTriggers, the agent’s own parser, which is the authority on what a condition means. An unknown condition, a malformed threshold, a quote or a semicolon fails the verb with exit status 2 before the first connect, and nothing is written.

The agent parses TRIGGERS again when it starts, because the envlist can be edited on the router by hand. On a value it cannot parse it exits non-zero with a mikroscope-agent: bad configuration: … line in the router log; under the on-failure policy RouterOS may retry it up to five times. No run with a bad TRIGGERS on the router is recorded.

Two of the four install routes hand the router something the CLI did not build, and each gets a check of its own before anything is written.

  • --agent-tar <file>, the image tar the release publishes, is read and inspected on your host first. It has to be a docker-save tar of exactly one image with one layer whose entrypoint is /mikroscope-agent, and its architecture has to match --arch; otherwise the verb stops, and for a mismatch it names the asset to download instead (--agent-tar … is a linux/arm64 image and --arch says arm: download the mikroscope-agent-arm.tar asset instead). That check says the tar is a mikroscope agent image of the right architecture. It does not say the tar is the one the release published — verify it against checksums.txt from the release, and its cosign signature if you use one, before you pass it.
  • --remote-image <reference> makes the router pull the image itself, so nothing is uploaded and no tar lands on the device. The reference is matched against a registry-reference pattern before it reaches the command line, because RouterOS takes it inside a quoted string on a ;-joined line. The router then needs to reach that registry over its own network, and it takes the registry host from /container/config registry-url, a setting global to the device and shared with every other container on it and ships as https://registry-1.docker.io. mikroscope never writes that setting. doctor reads it, and when the reference names a host the setting does not match it prints the one command to run (/container/config/set registry-url=https://ghcr.io, for the GHCR copy of the image) or says to use --agent-tar instead. The Docker Hub reference jmrplens/mikroscope-agent:1.0.0 carries no host and leaves the setting as the router has it. Trust in the image is trust in that registry: nothing in the CLI verifies what the router pulls.

plan --rsc writes the install as a RouterOS script for a router you reach only through WinBox or WebFig. It carries the same commands install runs, in the same order, with the same tags — and, when --token or MIKROSCOPE_TOKEN is set, the envlist line carries the token in clear, because the router needs it. The script says so in its own header. Treat the file the way you treat the token: do not commit it, do not paste it where it is logged, and delete it from the router’s Files after /import. Without a token it holds no secret, only the plan.

plan and install --dry-run mask the token in what they print to the terminal (value="(token)"); --rsc cannot, since the script has to run.

uninstall runs every removal newest first, ignoring what is already gone. Then it asks the router, in one connect, how many objects each step created are still there, prints one line per step with the count, and fails naming every step whose count is not zero (uninstall left objects behind: …). A removal that printed nothing is not evidence; the count is. status runs the same count on its own.

The container step is the slow one, and the order inside it is what keeps the count honest:

  • the container is stopped (guarded, because stopping a stopped container is an error) and removed;
  • /container/remove returns before the container is gone, and a /file/remove of the image issued meanwhile did nothing, silently (RB5009UG+S+, RouterOS 7.24.2, ) — so it waits up to 20 s for the container to vanish, then retries the file removal for up to 15 s;
  • the rest of the envlist goes, and the marker goes last, only once the file is gone.

If the file removal does not take, the marker stays, the count keeps including the envlist and the file, and uninstall says so instead of reporting clean.

A doctor → install → status → upgrade → uninstall round trip (make roundtrip) left the router’s /export byte-identical, compared by hash (verified on RB5009UG+S+, RouterOS 7.24.2, ).