Skip to content

Manual install: terminal

Install the agent one RouterOS command at a time, in the router’s terminal: for a router with no computer that runs the CLI, or to see and adapt every object. The commands are the ones install runs, with placeholders in capitals. For the same commands with your values filled in, use the Script generator.

Pick a value for each placeholder. The defaults are the ones install uses:

Placeholder Default Meaning
NAME mikroscope the install’s name
TAG mikroscope:NAME (managed by mikroscope) the comment on every object; keep this form so the CLI recognises the install
VETH veth-mikroscope the veth’s name
NET/30 172.30.10.0/30 an IPv4 /30 at its network address, unused on the router
GW_IP 172.30.10.1 the router’s end of the /30: the network address + 1
AGENT_IP 172.30.10.2 the agent’s end of the /30: the network address + 2
PORT 9123 the agent’s HTTP port
IFACE_LIST LAN the interface list the veth joins; none in the manifest when you skip the membership
ADDR_LIST LANs the address list the /30 joins; none in the manifest when you skip the membership
ENVLIST NAME-env the container’s envlist
DISK empty (internal flash) a disk slot, such as tmpfs or usb1; it prefixes the three paths below
MANIFEST_DIR mikroscope DISK/mikroscope on a disk
MANIFEST_FILE mikroscope/NAME.manifest.txt the install manifest
ROOT_DIR mikroscope/NAME the container’s root
IMAGE_REF registry-1.docker.io/jmrplens/mikroscope-agent:1.6.1 the image the router pulls (registry pull)
IMAGE_FILE NAME.tar the uploaded image tar (tar route); DISK/NAME.tar on a disk
RATE_HZ 10 the sampler rate, 1–100 Hz
BUFFER_S 60 the ring, 10–3600 s
MEM_LIMIT_MB 16 the agent’s Go memory limit, below
CAPTURE_MB 4 the triggered-capture budget, 0–256 MiB; 0 turns captures off
MEMORY_MAX 64M the container’s memory limit
START_ON_BOOT yes no for a root on a RAM disk, which a reboot empties
PRIVILEGED yes no loses the kernel log, the slab counts, the flash counters and the PMU
RESTART_MAX_COUNT 5 restarts after a failure
RESTART_INTERVAL 10s the wait between them
EXTRACT_TIMEOUT_S 120 how long to wait for RouterOS to extract the tar (tar route)
FLOOR_HZ none optional: one cadence for every level source
TRIGGERS none optional: trigger conditions (Triggered capture)
TOKEN none optional: the bearer token the agent asks for; a secret
CONTAINER_NAME empty (RouterOS names it) optional: the container’s name=
LAN_IP none with --expose only: the router’s LAN address

An empty default stays empty: on internal flash the manifest’s line is disk=, and without a container name container-name=, with nothing after the =.

MEM_LIMIT_MB is RATE_HZ × BUFFER_S × the size a sample takes in the ring × 2.5, rounded up, at least 16 and at most ¾ of MEMORY_MAX while that still leaves room for the ring. With a MEMORY_MAX of 64M that gives 16 at 10 Hz and 60 s, 25 at 50 Hz and 60 s, and 48 at 100 Hz and 60 s. The generator computes it for you.

Check the router. Read the release and the architecture, the container package and device mode:

/system/resource/print
/system/package/print where name="container"
/system/device-mode/print

version must be 7.24 or later and architecture-name one of arm64, arm or x86_64; the package must be listed and not disabled; device mode must show container: yes. Requirements has the fixes: the device-mode step needs a press of the reset or mode button, or a power cut, within 5 minutes; on CHR, power the virtual machine off and on.

Check for collisions. Each of these must print nothing, or a count of 0:

/interface/veth/print where name="VETH"
/container/envs/print count-only where list="ENVLIST"
/ip/address/print where address in NET/30
/ip/route/print where dst-address in NET/30
/file/print where name="MANIFEST_FILE"

And these must find what you name: the interface list, and the disk if you use one.

/interface/list/print where name="IFACE_LIST"
/disk/print

For a root on a RAM disk, create one first: /disk/add type=tmpfs tmpfs-max-size=64M slot=tmpfs, then use DISK tmpfs and START_ON_BOOT no. Whether the firewall drops the agent’s replies is on Firewall lists.

The manifest lists every object the next steps create, so that mikroscope uninstall and the removal below find them all. Leave out the object= line of any step you skip; for a list membership you skip, also write none after its iface-list= or addr-list=.

:local m "mikroscope-manifest=1\nname=NAME\ntag=TAG\ndisk=DISK\nveth=VETH\nsubnet=NET/30\nport=PORT\niface-list=IFACE_LIST\naddr-list=ADDR_LIST\nexpose=\ncontainer-name=CONTAINER_NAME\nremote-image=IMAGE_REF\ntoken=no\ndir=MANIFEST_DIR\nfile=MANIFEST_FILE\nobject=/interface/veth name=VETH\nobject=/ip/address interface=VETH\nobject=/interface/list/member interface=VETH list=IFACE_LIST\nobject=/ip/firewall/address-list list=ADDR_LIST address=NET/30\nobject=/container/envs list=ENVLIST\nobject=/container interface=VETH\ndir=ROOT_DIR\n"; :if ([:len [/file/find name="MANIFEST_FILE"]] > 0) do={ /file/set [find name="MANIFEST_FILE"] contents=$m } else={ /file/add name="MANIFEST_FILE" contents=$m }

Read it back with :put [/file/get [find name="MANIFEST_FILE"] contents]. RouterOS script explains each line.

/interface/veth/add name="VETH" address=AGENT_IP/30 gateway=GW_IP comment="TAG"
/ip/address/add address=GW_IP/30 interface="VETH" comment="TAG"

Optional. They let the agent’s replies past raw firewall rules that drop by interface list or address list; skip either when no rule of yours needs it, and drop its object= line from the manifest and write none after its iface-list= or addr-list=. A membership is not scoped to mikroscope: any other rule that matches the list matches the veth or the /30 too (Firewall lists).

/interface/list/member/add list="IFACE_LIST" interface="VETH" comment="TAG"
/ip/firewall/address-list/add list="ADDR_LIST" address=NET/30 comment="TAG"
/container/envs/add list="ENVLIST" key=MIKROSCOPE_TAG value="TAG"
/container/envs/add list="ENVLIST" key=RATE_HZ value="RATE_HZ"
/container/envs/add list="ENVLIST" key=BUFFER_S value="BUFFER_S"
/container/envs/add list="ENVLIST" key=PORT value="PORT"
/container/envs/add list="ENVLIST" key=ADDR value="AGENT_IP"
/container/envs/add list="ENVLIST" key=MEM_LIMIT_MB value="MEM_LIMIT_MB"
/container/envs/add list="ENVLIST" key=CAPTURE_MB value="CAPTURE_MB"

Add the optional entries you use:

/container/envs/add list="ENVLIST" key=FLOOR_HZ value="FLOOR_HZ"
/container/envs/add list="ENVLIST" key=TRIGGERS value="TRIGGERS"
/container/envs/add list="ENVLIST" key=TOKEN value="TOKEN"

The TOKEN value is stored in clear: treat it as readable by any RouterOS user with read, and keep other credentials out of the envlist.

/container/add remote-image="IMAGE_REF" interface="VETH" root-dir=ROOT_DIR envlist="ENVLIST" logging=yes start-on-boot=START_ON_BOOT restart-policy=on-failure restart-max-count=RESTART_MAX_COUNT restart-interval=RESTART_INTERVAL memory-max=MEMORY_MAX privileged=PRIVILEGED ignore-remote-image-change=yes comment="TAG"

RouterOS pulls the image for its own architecture. The router needs to reach the registry and to have room in RAM for the layers.

Optional: to reach the agent on the router’s LAN address, add the two rules --expose writes (Expose on the LAN). They need a TOKEN in the envlist.

Write the manifest again first, so that it records them: expose=LAN_IP, token=yes and one object= line per rule.

:local m "mikroscope-manifest=1\nname=NAME\ntag=TAG\ndisk=DISK\nveth=VETH\nsubnet=NET/30\nport=PORT\niface-list=IFACE_LIST\naddr-list=ADDR_LIST\nexpose=LAN_IP\ncontainer-name=CONTAINER_NAME\nremote-image=IMAGE_REF\ntoken=yes\ndir=MANIFEST_DIR\nfile=MANIFEST_FILE\nobject=/interface/veth name=VETH\nobject=/ip/address interface=VETH\nobject=/interface/list/member interface=VETH list=IFACE_LIST\nobject=/ip/firewall/address-list list=ADDR_LIST address=NET/30\nobject=/ip/firewall/nat chain=dstnat dst-address=LAN_IP dst-port=PORT protocol=tcp\nobject=/ip/firewall/filter chain=forward dst-address=AGENT_IP dst-port=PORT protocol=tcp\nobject=/container/envs list=ENVLIST\nobject=/container interface=VETH\ndir=ROOT_DIR\n"; :if ([:len [/file/find name="MANIFEST_FILE"]] > 0) do={ /file/set [find name="MANIFEST_FILE"] contents=$m } else={ /file/add name="MANIFEST_FILE" contents=$m }

Then add the two rules:

/ip/firewall/nat/add chain=dstnat dst-address=LAN_IP protocol=tcp dst-port=PORT action=dst-nat to-addresses=AGENT_IP to-ports=PORT comment="TAG"
:local d [/ip/firewall/filter/find chain=forward action=drop]; :if ([:len $d] > 0) do={ /ip/firewall/filter/add chain=forward dst-address=AGENT_IP protocol=tcp dst-port=PORT connection-nat-state=dstnat action=accept comment="TAG" place-before=($d->0) } else={ /ip/firewall/filter/add chain=forward dst-address=AGENT_IP protocol=tcp dst-port=PORT connection-nat-state=dstnat action=accept comment="TAG" }
/container/start [find comment="TAG"]

On the router:

/container/print where comment="TAG"
/log/print where topics~"container"
:put ([/tool/fetch url="http://AGENT_IP:PORT/healthz" output=user as-value]->"data")

After a registry pull, RouterOS downloads and extracts the image before it starts the container. Until the log shows *** download/extract done, the container shows the E (DOWNLOADING/EXTRACTING) flag and the last command prints failure: Invalid argument: run the three again.

Then the container shows the R (running) flag. The log ends with *** started /mikroscope-agent and the agent’s own line, mikroscope-agent <version> …: kernel …, 10 Hz, ring 60 s, listening on AGENT_IP:PORT, …. The last command prints {"ok":true,…}. From a computer on the router’s LAN, curl http://AGENT_IP:PORT/healthz answers the same, and mikroscope status recognises the install.

Run the removals, newest first. The first stops and removes the container and its root and envlist; the last removes the manifest and the MANIFEST_DIR directory when nothing else is in it, and stops, keeping the manifest, if any object with the tag is left. If you exposed the agent, remove its two rules first:

/ip/firewall/filter/remove [find chain=forward dst-address="AGENT_IP" dst-port="PORT" protocol="tcp" comment="TAG"]
/ip/firewall/nat/remove [find chain=dstnat dst-address="LAN_IP" dst-port="PORT" protocol="tcp" comment="TAG"]

Then the rest:

:local had [:len [/container/find comment="TAG"]]; :do { /container/stop [find comment="TAG"] } on-error={}; :local s 0; :while (([:len [/container/find comment="TAG" running]] + [:len [/container/find comment="TAG" stopping]]) > 0 && $s < 30) do={ :delay 1s; :set s ($s + 1) }; /container/remove [find comment="TAG"]; :local i 0; :while ([:len [/container/find comment="TAG"]] > 0 && $i < 20) do={ :delay 1s; :set i ($i + 1) }; :if ($had > 0) do={ :local r 0; :while ([:len [/file/find name="ROOT_DIR"]] > 0 && ([:len [/container/find root-dir="/ROOT_DIR"]] + [:len [/container/find root-dir="ROOT_DIR"]]) = 0 && $r < 10) do={ :delay 1s; :set r ($r + 1) }; :if ([:len [/file/find name="ROOT_DIR"]] > 0 && ([:len [/container/find root-dir="/ROOT_DIR"]] + [:len [/container/find root-dir="ROOT_DIR"]]) = 0) do={ :do { /file/remove [find name="ROOT_DIR"] } on-error={} } }; :if ([:len [/container/envs/find list="ENVLIST" key="MIKROSCOPE_TAG" value="TAG"]] > 0) do={ /container/envs/remove [find list="ENVLIST" key!="MIKROSCOPE_TAG"]; /container/envs/remove [/container/envs/find list="ENVLIST" key="MIKROSCOPE_TAG" value="TAG"] }
/ip/firewall/address-list/remove [find list="ADDR_LIST" address="NET/30" comment="TAG"]
/interface/list/member/remove [find interface="VETH" list="IFACE_LIST" comment="TAG"]
/ip/address/remove [find interface="VETH" comment="TAG"]
/interface/veth/remove [find name="VETH" comment="TAG"]
:if (([:len [/ip/firewall/address-list/find comment="TAG"]] + [:len [/interface/list/member/find comment="TAG"]] + [:len [/ip/firewall/nat/find comment="TAG"]] + [:len [/ip/firewall/filter/find comment="TAG"]] + [:len [/ip/firewall/raw/find comment="TAG"]] + [:len [/ip/firewall/mangle/find comment="TAG"]] + [:len [/ip/route/find comment="TAG"]] + [:len [/container/mounts/find comment="TAG"]] + [:len [/ip/address/find comment="TAG"]] + [:len [/interface/veth/find comment="TAG"]] + [:len [/interface/list/find comment="TAG"]] + [:len [/disk/find comment="TAG"]] + [:len [/container/find comment="TAG"]] + [:len [/container/envs/find list="ENVLIST" key="MIKROSCOPE_TAG" value="TAG"]]) > 0) do={ :error "mikroscope: objects tagged TAG remain, so MANIFEST_FILE stays" }; :if ([:len [/file/find name="MANIFEST_FILE"]] > 0) do={ :if ([:typeof [:find [/file/get [find name="MANIFEST_FILE"] contents] "\ntag=TAG\n"]] = "num") do={ :if ([:len [/file/find name="ROOT_DIR"]] > 0 && ([:len [/container/find root-dir="/ROOT_DIR"]] + [:len [/container/find root-dir="ROOT_DIR"]]) = 0) do={ :do { /file/remove [find name="ROOT_DIR"] } on-error={} }; /file/remove [find name="MANIFEST_FILE"] } }; :if ([:len [/file/find name~"^MANIFEST_DIR/"]] = 0) do={ /file/remove [find name="MANIFEST_DIR" type="directory"] }

If a tar is still there because extraction timed out, remove it too: /file/remove [find name="IMAGE_FILE"]. Then check that nothing is left; this prints 0:

:put ([:len [/interface/veth/find comment="TAG"]] + [:len [/ip/address/find comment="TAG"]] + [:len [/interface/list/member/find comment="TAG"]] + [:len [/ip/firewall/address-list/find comment="TAG"]] + [:len [/ip/firewall/nat/find comment="TAG"]] + [:len [/ip/firewall/filter/find comment="TAG"]] + [:len [/container/find comment="TAG"]] + [:len [/container/envs/find list="ENVLIST"]] + [:len [/file/find name="MANIFEST_FILE"]])

mikroscope uninstall --router … --yes does the same from a computer with the CLI, and verifies it: Upgrade and uninstall.