NETSetup flash-mikrotik command flashes a stock MikroTik hAP-class device into a single-box router + WiFi AP + 4-port switch in one shot. 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) configures:

Section Effect

Bridge

Single bridge named bridge containing ether2, ether3, ether4, ether5, wifi1, wifi2. All six interfaces switched together.

Interface lists

LAN = bridge. WAN = ether1. Used by firewall rules + NAT.

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

ether1 runs /ip dhcp-client disabled=no. Plug your upstream into ether1; device gets internet via DHCP from the ISP.

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.

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";

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

Uses osisa.NETCommand.Commands.MikroTik.MikroTikSystem.UploadFilepscp -batch -scp -pw <admin-pwd> -hostkey <auto-detected> <local> admin@<ip>:<remote>.

  • User hardcoded as admin@ in netbase library.

  • HostKeyRetryCmd runs pscp once without -hostkey, regex-parses the SHA256 fingerprint from the error, retries with -hostkey "<fingerprint>". Fully automatic.

  • 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. Done log

-

Final LogInformation with applied settings + verification instructions.

Run wraps phases 3-7 in try/finally so the NIC reverts to DHCP on any failure path.

Code map

File Role

src/NETSetup/CLI/Commands/MikrotikCommands.cs

Entry point. Holds Cocona registration, Run orchestrator, and every helper (ApplyDefaults, DecideDeviceIp, ListEthernetAdapters, IsPhysicalEthernet, SelectAdapter, SetStaticIpForFactoryRoute, RevertNicToDhcp, ProbeWithRetries, Countdown, UploadRsc, ResetWithRsc, 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

Open Winbox to 192.168.88.1 (factory) or 10.0.0.1 (post-flash) → System → History. Shows whether run-after-reset ran or errored. Common errors:

  • 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).

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.