NETSetup flash-mikrotik command flashes a stock MikroTik device into a single-box router
WiFi AP + switch in one shot. The generated .rsc is board-tolerant: works on hAP ax2 (ether1-5 + wifi1/wifi2), cAP ax (ether1+ether2 + wifi1/wifi2), and other RouterOS-7 boards without per-model code paths. This doc covers what it does, how to use it, the code map, the test map, and how to recover when it fails.

Hardware refs

Sticker

Bottom of device, three values:

Field Example Purpose

Admin

CPDCYY96E8

Current admin password on the device. Pass to --admin-password.

Wifi

YRHQ80LCYT

Out-of-box WPA passphrase for the factory WiFi. Not used after flash.

MAC

XX:XX:XX:XX:XX:XX

Optional --mac value. When provided the host ARP-scans for it; when omitted the host goes direct to the factory IP 192.168.88.1.

Factory reset

Hold reset button BEFORE plugging in power. Keep holding ~10s until LED flashes. Then plug power in. Device boots into stock RouterOS at 192.168.88.1 on ether2-5.

Cabling

  • Plug PC LAN into one of ether2, ether3, ether4, ether5 (NOT ether1).

  • ether1 is WAN side. Stock device thinks anything plugged there is the ISP and will not talk to your PC.

  • ether2-5 is the LAN bridge. Stock RouterOS serves DHCP 192.168.88.x and answers at 192.168.88.1 on this side.

  • Disable WiFi on your PC. Disable VPN. Avoid putting a switch in between (works but slows ARP).

After flash, same physical mapping holds: ether1 = WAN, ether2-5 + WiFi = LAN, but the LAN scheme changes to 10.0.0.0/24 instead of factory 192.168.88.0/24.

Quick start

Direct contact (PC cabled straight into ether2-5, no MAC discovery):

netsetup flash-mikrotik --admin-password CPDCYY96E8

With MAC discovery (PC and device on same L2 segment, ARP scan resolves MAC → IP):

netsetup flash-mikrotik --admin-password CPDCYY96E8 --mac F4:1E:57:FF:B7:A2

Custom SSID + WiFi pwd + hostname + post-flash admin pwd:

netsetup flash-mikrotik `
    --admin-password CPDCYY96E8 `
    --ssid CustomerNet `
    --wifi-password Hunter2! `
    --hostname Cust_AP_01 `
    --flashed-new-admin-password 'StrongPwd!2026'

CLI flags

Flag Required Meaning

--admin-password

yes

Current admin password on the device. Stock unit → sticker Admin value. Already-configured unit → whatever the last admin password was. WRONG value → "FATAL ERROR: Configured password was not accepted" → friendly auth-hint exit.

--mac

no

Device MAC. When set the host runs an ARP scan for it across all Up subnets first; on hit it uses the resolved IP, on miss it falls back to the factory-IP route. When omitted the host goes straight to the factory-IP route.

--ssid

no (default Liberator)

WPA SSID applied to both 5GHz (wifi1) and 2.4GHz (wifi2) interfaces.

--wifi-password

no (default 12345678)

WPA passphrase. Used by both bands. Min 8 chars per WPA spec.

--hostname

no (default Lib_AP_<rand4>)

/system identity value. Random 4-digit suffix when defaulted so multiple devices on the same LAN do not collide.

--flashed-new-admin-password

no (default default4244$)

New admin password written into the .rsc and applied AFTER reset-configuration. This is the post-flash login pwd, NOT the same as --admin-password.

What gets applied

The .rsc rendered by MikrotikRscBuilder.Build (in src/NETSetup/Helpers/MikrotikRscBuilder.cs) is board-tolerant:

  • /system identity set name=…​ runs FIRST (after :delay 15s) so the device is identifiable in Winbox Neighbors view even if a later step fails.

  • /user set [find name=admin] password=…​ runs second so login is possible on partial failure.

  • Every /…​ operation wrapped in :do {…​} on-error={:log error …​} → missing interface stops one step, not the whole script.

  • Bridge ethernet ports discovered via :foreach e in=[/interface ethernet find where name!="ether1"] instead of hardcoded ether2/3/4/5 → same .rsc works on cAP ax (only ether1+ether2) and hAP ax2 (ether1-5).

  • WiFi radios discovered via :foreach w in=[/interface wifi find] → tolerates boards with one radio.

  • :log info "NETSetup-flash: <phase>" markers at every phase → on partial failure /log print where message~"NETSetup-flash" on the device pinpoints which step blew up.

Sections configured:

Section Effect

Bridge

Single bridge named bridge. Bridge ports added by :foreach loops over every ethernet except ether1 plus every wifi interface. On hAP ax2 that resolves to ether2-5 + wifi1/wifi2; on cAP ax just ether2 + wifi1/wifi2. Missing interfaces logged as warnings.

Interface lists

LAN = bridge. WAN = BOTH ether1 (untagged) AND ether1-vlan10 (tagged). Used by firewall rules + NAT. Two WAN members so box auto-works on plain DHCP net OR behind XGS-PON modem - see WAN row below.

WAN VLAN sub-iface

/interface vlan add name=ether1-vlan10 interface=ether1 vlan-id=10 → tagged WAN sub-iface riding on ether1. Const MikrotikRscBuilder.WanVlanId = "10". Added to WAN interface-list alongside untagged ether1.

WiFi

wifi1 5GHz-ax (20/40/80 MHz, country=Switzerland), wifi2 2.4GHz-ax (20/40 MHz). Both: WPA2-PSK + WPA3-PSK, fast-transition (ft=yes, ft-over-ds=yes), enabled.

LAN IP

10.0.0.1/24 on the bridge. Constants exposed as MikrotikRscBuilder.LanGateway / LanCidr / LanNetwork for tests.

DHCP server

Pool 10.0.0.2-10.0.0.254 on the bridge. Hands out gateway=10.0.0.1, dns-server=10.0.0.1.

WAN (dual-WAN, auto-detect)

dhcp-client runs on BOTH ifaces:

  • /ip dhcp-client add interface=ether1 disabled=no → untagged. Leases when box plugged into a plain DHCP network.

  • /ip dhcp-client add interface=ether1-vlan10 disabled=no → tagged VLAN 10. Leases when box sits behind an XGS-PON modem that delivers WAN tagged on VLAN 10.

Same physical wire (ether1) carries both. ISP delivers WAN EITHER untagged OR tagged-on-10, never both, so only ONE dhcp-client ever gets a lease → no default-route conflict. Box auto-works in either deployment, no manual reconfig.

DNS

/ip dns set servers=1.1.1.1,8.8.8.8 allow-remote-requests=yes → device acts as DNS resolver for LAN clients.

NAT

/ip firewall nat add chain=srcnat action=masquerade out-interface-list=WAN → classic masquerade. Uses the WAN LIST (not literal ether1) so NAT covers whichever WAN iface (ether1 or ether1-vlan10) actually carries traffic.

Firewall

Stateful filter: established/related accepted, invalid dropped, ICMP allowed, LAN trusted, WAN-init dropped on input + forward.

System

Timezone Europe/Zurich. Hostname from --hostname. Login banner suppressed.

Admin user

/user set [find name=admin] password="<flashed-new-admin-password>" → post-flash login uses the new pwd.

MAC server

/tool mac-server and /tool mac-server mac-winbox restricted to LAN list.

Constants in MikrotikRscBuilder:

public const string LanGateway   = "10.0.0.1";
public const string LanCidr      = "10.0.0.1/24";
public const string LanNetwork   = "10.0.0.0/24";
public const string DhcpPoolRange = "10.0.0.2-10.0.0.254";
public const string WanVlanId     = "10";  // tagged WAN sub-iface ether1-vlan10 for XGS-PON

Flow phases

MikrotikCommands.Run (entry from CLI handler or test) walks these phases:

Phase Method Detail

1. Defaults

ApplyDefaults

Pure: fills nullable optional params with published defaults. Hostname suffix injectable for tests.

2. Putty

EnsurePuttyInstalled

Idempotent choco install putty.install (provides pscp).

3. Resolve IP

ResolveDeviceIp

Returns (deviceIp, appliedNic?).

  • --mac set → ArpScanner.FindIPByMAC. Hit → return; miss → log warning, fall through.

  • No mac (or ARP miss) → factory-IP route:

    • ListEthernetAdapters → filter via IsPhysicalEthernet (rejects WSL, TAP-, Tailscale, ZeroTier, Bluetooth, Loopback, Pseudo, Network Bridge, Multiplexor, WFP, LightWeight Filter, QoS Packet Scheduler, Miniport; rejects ifIndex ⇐ 0).

    • LogCandidateAdapters numbered list.

    • SelectAdapter: 1 candidate → auto. Many → UserInteraction.UserInputInt prompt. 0 → exit.

    • SetStaticIpForFactoryRoute → PowerShell New-NetIPAddress 192.168.88.10/24 on chosen NIC.

    • ProbeWithRetries → 5x ArpScanner.IsIpOccupied("192.168.88.1") with 1s spacing (NIC needs a beat after Set-NetIPAddress).

    • DecideDeviceIp pure: hit → return factory IP, miss → failure with cabling guidance.

4. Render .rsc

WriteRscFile

MikrotikRscBuilder.Build → writes to Path.GetTempPath()/flash-mikrotik-<guid>.rsc via osisa.IO.Contracts.IFileOperator.

5. Upload

UploadRsc + local PscpSftpUpload

Bypasses the netbase MikroTikSystem.UploadFile (hardcoded -scp, rejected by RouterOS 7 with "Server refused to start a shell/command" because ROS has no shell to run remote scp). Local helper runs:

pscp -batch -sftp -pw <admin-pwd> [-hostkey <fingerprint>] <local> admin@<ip>:<bare-name>.

  • SFTP, not SCP - RouterOS 7’s SSH server supports only SFTP for file transfer.

  • Bare remote filename, no flash/ prefix. RouterOS SFTP root maps to the device file storage; subdir paths return "unable to open …​: unknown error code".

  • Two-pass hostkey: first attempt omits -hostkey, regex-parses SHA256:…​ from the error, retries with -hostkey "<fingerprint>". Mirrors the netbase HostKeyRetryCmd flow.

  • User hardcoded as admin@ (matches the netbase convention).

  • Result inspected by ContainsPuttyError (markers: FATAL ERROR, Network error, Access denied, Configured password was not accepted, !trap, Bad command, syntax error, …​). Wrapped in try/catch → ExtractCommandOutcome unwraps AggregateException.InnerException.MessageExitWithAuthHint surfaces "the --admin-password you passed is NOT the current admin password" on auth failure.

6. Reset

ResetWithRsc

Uses MikroTikSystem.ResetConfiguration (tik4net API on TCP/8728, NOT SSH).

  • Sends /system reset-configuration keep-users=yes no-defaults=yes skip-backup=yes run-after-reset=<file>.

  • keep-users=yes preserves the admin account so we can re-login post-flash; no-defaults=yes wipes everything else; skip-backup=yes does NOT save a pre-reset backup; run-after-reset executes the .rsc on first boot after reset.

  • Stdout cleaned by StripNetbaseMacNoise → regex strips Invalid MAC address '<ip>'. prefix that the netbase MikroTikCommandBase ctor leaks when the host arg is an IP.

  • Same auth-hint path on failure.

7. Revert NIC

RevertNicToDhcp

Powershell Remove-NetIPAddress + Set-NetIPInterface -Dhcp Enabled on the NIC we set static. Best-effort: warns on non-success but does NOT exit.

8. Verify alive

VerifyPostFlashAlive

2-min initial grace (device reboot + run-after-reset apply), then ICMP poll of 10.0.0.1 every 5s with 3s per-ping timeout, up to 2 min total. Logs per-attempt status. Final log line distinguishes "Flash applied + verified" (alive) from a warning with manual-verify steps.

9. Done log

-

Either INFO success line or WARN line with SSID-check / replug / Winbox fallback steps.

Run wraps phases 3-8 in try/finally AND registers an AppDomain.CurrentDomain.ProcessExit handler so the NIC reverts to DHCP even on host.ExitFailure (which calls Environment.Exit and bypasses finally). Static fields s_pendingNicRevert + s_pendingNicHost are armed by SetStaticIpForFactoryRoute and cleared by RevertNicToDhcp.

Code map

File Role

src/NETSetup/CLI/Commands/Network/MikrotikCommand.cs

Entry point. Holds Cocona registration, Run orchestrator, and every helper (ApplyDefaults, DecideDeviceIp, ListEthernetAdapters, IsPhysicalEthernet, SelectAdapter, SetStaticIpForFactoryRoute, RevertNicToDhcp, OnProcessExitRevertNic, ProbeWithRetries, Countdown, UploadRsc, PscpSftpUpload, ResetWithRsc, VerifyPostFlashAlive, DefaultPingProbe, ContainsPuttyError, StripNetbaseMacNoise, ExtractCommandOutcome, ExitWithAuthHint).

src/NETSetup/Helpers/MikrotikRscBuilder.cs

Pure .rsc template renderer. Inputs: ssid, wifiPassword, hostname, adminPassword. Output: RouterOS script string. Constants for LAN gateway / cidr / DHCP pool.

src/NETSetup/Helpers/ArpScanner.cs

Win32 P/Invoke SendARP. Two functions used here: FindIPByMAC(targetMAC) (resolve MAC → IP across Up NICs), IsIpOccupied(ip) (single-IP ARP probe).

../NETCommand/src/osisa.NETCommand/Commands/MikroTik/MikroTikSystem.cs

External (netbase). UploadFile builder (pscp upload + auto-hostkey retry) and ResetConfiguration builder (tik4net API call).

../NETCommand/src/osisa.NETCommand/Commands/MikroTik/MikroTikCommandBase.cs

External (netbase). Base class for every API-based MikroTik command. Hardcodes user as admin (line 25). No override exposed.

Test map

Test Covers

MikrotikCommandsTests.AddAll_*

Cocona registration smoke test.

MikrotikCommandsTests.ApplyDefaults_*

All-null → published defaults; all-set → caller values preserved; hostname-set → suffix generator NOT invoked.

MikrotikCommandsTests.DecideDeviceIp_*

ARP hit → arp ip; ARP miss + factory occupied → factory ip + IsFallback; whitespace ARP → miss; ARP miss + factory empty → failure with actionable msg.

MikrotikCommandsTests.IsPhysicalEthernet_*

Real NICs (Intel/Realtek/Broadcom + Hyper-V vEthernet for ext-switch use case) accepted; WSL/TAP/Tailscale/Bluetooth/Network Bridge/Multiplexor/WFP rejected; ifIndex ⇐ 0 rejected.

MikrotikCommandsTests.SelectFirstEthernet_*

Empty → null; multiple → first.

MikrotikCommandsTests.SelectAdapter_*

Empty → error; single → auto-pick (no prompt); multiple → prompt seam called and returned index used; out-of-range index → actionable error.

MikrotikCommandsTests.LogAdapterChoice_*

Logs name + description + MAC + ifIndex + "(N other adapter(s))" line. Verified via CaptureLogMessages helper that hooks the ILogger.Log formatter delegate.

MikrotikCommandsTests.Countdown_*

N-second loop calls injectable sleep N times + emits N log lines. Zero seconds → no-op.

MikrotikCommandsTests.ProbeWithRetries_*

First probe succeeds → returns true with no sleep. Later probe succeeds → returns true after N-1 sleeps. All probes fail → returns false; never sleeps after the final attempt.

MikrotikCommandsTests.ContainsPuttyError_*

Null/empty/clean output → false. Every known marker (PuTTY + RouterOS API: FATAL ERROR, !trap, Bad command, syntax error, etc) → true.

MikrotikCommandsTests.StripNetbaseMacNoise_*

Regex correctly strips Invalid MAC address '<ip-with-dots>'. even when the IP itself contains dots. Clean input passes through unchanged.

MikrotikCommandsTests.FlashMikrotik_AgainstRealDevice_ShouldComplete

[Ignore]-d end-to-end against a physical device. Toggle off, set DataRow MAC + factory password, run.

MikrotikRscBuilderTests.Build_*

Pure-function template renderer: every parameter (ssid, passphrase, hostname, admin pwd) appears verbatim in the output.

Verification

After the command exits "Done. Reset-configuration command sent…​":

  1. Wait 60-120s. Device reboots, applies factory defaults (because no-defaults=yes), then runs run-after-reset=<file> to apply our .rsc.

  2. Look for SSID Liberator (or whatever you passed to --ssid) on phone/laptop WiFi list. SSID visible → .rsc applied → success.

  3. Or cable PC into ether2-5, get DHCP lease in 10.0.0.0/24, ping 10.0.0.1.

  4. Or open Winbox. If device responds at 10.0.0.1 → success. If responds at 192.168.88.1 → .rsc failed to apply on first boot, device fell back to factory.

Troubleshooting

"Configured password was not accepted"

Three causes:

  • Wrong sticker copy/paste.

  • RouterOS 7.x first-login forced password change. Stock device asks "Please set new password" interactively; pscp -batch cannot answer. Fix: open Winbox once, accept the prompt by setting the sticker pwd as the new pwd, then rerun flash-mikrotik.

  • Device was previously configured → sticker irrelevant → use whatever pwd the previous config set.

"ARP probe 192.168.88.1 …​ no reply" 5x

  • Cable plugged into ether1 (WAN) → move to ether2-5.

  • PC NIC blocked by Windows firewall on the new subnet → Windows usually prompts on first attach; click "private network".

  • Hyper-V external switch is the picked adapter → works as long as the underlying physical NIC is bridged AND linked. If the physical NIC has no link, the vSwitch has no carrier.

Multiple adapters in candidate list

SelectAdapter prompts for an index. Pick the one whose Description matches the cable you plugged in. Hyper-V external-switch vEthernet IS valid and shows up in the list.

SSID never appears OR device still shows Identity "MikroTik" + IP 0.0.0.0 in Winbox Neighbors

Symptom: device booted, Winbox Neighbors sees it, but Identity didn’t change and bridge has no IP.

First check the :log info markers we emit:

/log print where message~"NETSetup-flash"

Output tells you which phase aborted. Common failures:

  • Country code mismatch (Switzerland → change in MikrotikRscBuilder.Build if relocating).

  • WiFi interface name not wifi1/wifi2 on older firmware → rerun on RouterOS 7.13+.

  • Bridge port already exists (rare; happens if keep-users=yes accidentally preserved a bridge from prior config).

  • On older builds: hardcoded ether2/3/4/5 bridge port adds halted the script on cAP-class boards (no ether3-5). Fixed - current MikrotikRscBuilder discovers ports via :foreach.

pscp "Server refused to start a shell/command"

Hit before SFTP migration. RouterOS 7 has no shell, so pscp -scp (which opens an SSH exec channel) is refused. Local PscpSftpUpload uses -sftp now; this error should be gone. If it returns, verify osisa.NETCommand was not updated to do its own thing in UploadRsc and that PscpSftpUpload is still the active path.

pscp "unable to open <name>: unknown error code"

RouterOS SFTP doesn’t accept subdir paths. Remote name must be a bare filename. Make sure the caller passes rscPath.Name (not flash/<name>) to UploadRsc.

Reset-configuration stdout has "Invalid MAC address '192.168.88.1'."

Cosmetic only. MikroTikCommandBase ctor in netbase tries new MacAddress(host) first and accumulates the parse exception into its result string. StripNetbaseMacNoise removes this prefix from logs. Not a real failure.

Manual fallback

When flash-mikrotik is broken or unavailable:

  1. Cable PC into ether2-5. PC gets DHCP lease in 192.168.88.0/24.

  2. Open Winbox → connect to 192.168.88.1 as admin / sticker pwd. Or use WebFig in browser.

  3. Open the netbuild repo, navigate to osisa.Nuke.Commands.Tests/MikroTikTests/TestFiles/Liberator_AP.rsc (or generate a fresh one by running MikrotikRscBuilder.Build in a debugger).

  4. Adjust SSID / passphrase / hostname / admin pwd in the .rsc as needed.

  5. In Winbox → Files: drag the .rsc onto the file list to upload.

  6. Open Terminal: /system reset-configuration keep-users=yes no-defaults=yes skip-backup=yes run-after-reset=Liberator_AP.rsc.

  7. Wait 60-120s. Verify as in Verification.

For ongoing changes after the device is at 10.0.0.1, plug ether1 into your network and SSH/Winbox to whatever IP your upstream DHCP hands out, OR connect a phone to the Liberator WiFi and Winbox to 10.0.0.1.