Flashfig on linux

Hello !

I was looking for an Linux solution for Flashfig. As I couldn’t find one, I tried to build one myself.

I am pretty close now as I succeeded to push a configuration via Flashfig with a standard DHCP and TFTP server. Flashfig simply use the BOOTP protocol with option 60,128,129, next-server and boot filename. It then retrieve the configuration file with TFTP.

However, there is a tricky part. The configuration is compressed and I cannot determine what compression is used. So I am unable to fully reproduce a Linux solution of Flashfig. My current solution is to sniff the compressed configuration sent by the Windows Flashfig tool and retrieve the hex stream of the compressed file. After that, I am able to create a file with this hex stream and use it on my own TFTP server.

As you notice, it is not very handy. I would appreciate if anyone can help me find how the configuration is compressed. For the moment, I think I identify the file signature at the beginning of every file sent. It always start with 0x31 0x32 0x33 0x20 0xff 0xfe 0x04 0x00.

Cheers

Hi,
I too want to do this… but from the DHCP/TFTP services on another mikrotik router.
IE, I want to be able to send the rsc file from a mikrotik “server” via flashfig when I plug-in new routers.
I can get the file transfer via bootp, but don’t know how to compress the file beforehand.

Did you get anywhere with this?

Yes, I can compress the file but it is not handy. I do a first flashfig with the Windows Mikrotik tools and I capture the payload with Wireshark.

Then, I reuse the payload on my own tftp server and it’s working.

Did you ever solve this issue with the format of the file?

We use Flashfig on LTS Ubuntu via wine.

Works better then is does in win 10.

Have some scripted ssh for re-provisoning etc

Getting flash fig to work is a work of art all it’s self.

Keep it simple as possible just get the device online and pull the config from an ftp.

I’ve attached what we use.

Other tip:

Uses the Atom editor.
Really nice editor and someone wrote a microtik scripting module.

Finally the attached file was actually generated with
$ /export compact file=flashfig.rsc
You need to start with a clean (dhcp on ether 1, nothing else) Hap or whatever.
Add in the scripts and tasks you need then export.
flashfig.rsc (1.1 KB)

Hello,

@rcy, @Nicka Ketta and other, did you find how is compressed file (gzip, lzma, zip, 7zip, ect.)?

Best Regards,
Kamil

RSC is a plaintext file, what are you guys talking about?

Probably they are referring to what is actually transmitted to the device(s), likely flashfig “converts” the source .rsc to a binary blob or some kind (compressed or more likely encoded) and transmits over the lan this latter.

Well hacking up your own netinstall alternative, good luck. Sorry but we can’t help.

Hi,

did someone figgured this out? I want to achieve the same thing.
I need to be able to get zero config to mikrotik device without using windows.

Thanks

Sorry for digging up this necro thread again.

I've been working on the same problem and managed to identify the compression algorithm by reverse-engineering flashfig.exe. Sharing my findings here since this thread has been unanswered for years.

TL;DR

The FlashFig config payload is LZMA compressed with a 6-byte Mikrotik header prepended. You can reproduce it entirely with standard Linux tools.

The Wire Format

The magic bytes @rcy identified (31 32 33 20 ff fe 04 00) decode as:

Offset 0-3:  31 32 33 20     "123 " — Mikrotik magic header
Offset 4-5:  ff fe           Format marker
Offset 6:    04              LZMA properties byte
Offset 7-10: 00 00 08 00     LZMA dictionary size (LE) = 524288
Offset 11-18:                Uncompressed size (8 bytes, LE)
Offset 19+:                  LZMA compressed data stream

The LZMA properties byte 0x04 encodes: lc=4, lp=0, pb=0 (formula: (pb×5+lp)×9+lc = 4).

How to Reproduce the Compressed Config on Linux

# Compress your .rsc with the exact LZMA parameters FlashFig uses
lzma --format=alone --lzma1=lc=4,lp=0,pb=0,dict=524288 < config.rsc > config.lzma

# Prepend the 6-byte Mikrotik header
printf '123 \xff\xfe' | cat - config.lzma > config.flashfig

# Serve config.flashfig via your TFTP server as the boot filename

Combine this with a DHCP/BOOTP server (dnsmasq works) configured with options 60, 128, 129, next-server, and boot filename pointing to the compressed file, and you have a fully Linux-native FlashFig replacement.

How I Found This — Steps to Reproduce

All analysis was done on flashfig.exe 7.22.1 (534KB, PE32 executable). The same approach should work on other versions.

1. String analysis

strings flashfig.exe | grep -iE 'flash|config|tftp|convert|data'

Key strings found: Flashfig, Flashfigged, flash data size, config, convertor, Fig DataSource, Fig DataTarget, TFTP transfer failed, tftp transfer ok.

Notably absent: any explicit references to zlib, lzma, gzip, or any named compression library. The LZMA SDK compiles as plain C with no identifying strings.

2. Find the "123 " header construction

Using objdump -d -M intel flashfig.exe, I located code that constructs the header byte-by-byte at VA 0x411ec7:

mov BYTE PTR [ebp-0x20], 0x31    ; '1'
call 0x42f118                     ; append
mov BYTE PTR [ebp-0x20], 0x32    ; '2'
call 0x42f118                     ; append
mov BYTE PTR [ebp-0x20], 0x33    ; '3'
call 0x42f118                     ; append
mov BYTE PTR [ebp-0x20], 0x20    ; ' '
call 0x42f118                     ; append

This confirmed the header is hardcoded ASCII "123 ".

3. Trace the encoding function

Following the code path after the header construction, the config data is passed to a function at VA 0x40e3d8, which calls into 0x43d3d4 — a compression wrapper that dispatches through a function table at VA 0x45d100.

The table has two entries (id=0 → 0x43d8b4, id=1 → 0x43d5d4). The id=1 function at 0x43d5d4 is the encoder.

4. Identify LZMA parameters

The encoder function at 0x43d5d4 initializes a compressor with these constants:

mov DWORD PTR [ebp-0x88], 0x9       ; level = 9
mov DWORD PTR [ebp-0x84], 0x80000   ; dict = 524288 (512KB)
mov DWORD PTR [ebp-0x80], 0x4       ; lc = 4
mov DWORD PTR [ebp-0x7c], 0x0       ; lp = 0
mov DWORD PTR [ebp-0x78], 0x0       ; pb = 0
mov DWORD PTR [ebp-0x70], 0x111     ; fb = 273 (max fast bytes)
mov DWORD PTR [ebp-0x68], 0x3       ; btMode = 3
mov DWORD PTR [ebp-0x5c], 0x1       ; endMarker = 1

These are unmistakably LZMA encoder properties. Level 9, 512KB dictionary, 273 fast bytes — all maximum settings.

5. Confirm with range coder signatures

# Search for LZMA range coder kTopValue (0x01000000)
python3 -c "
import struct
with open('flashfig.exe','rb') as f: data=f.read()
pattern = struct.pack('<I', 0x01000000)
print(f'kTopValue refs: {data.count(pattern)}')
pattern2 = struct.pack('<I', 0x800)
print(f'kBitModelTotal refs: {data.count(pattern2)}')
"

Result: 22 references to kTopValue (0x01000000) and ~27 references to kBitModelTotal (0x800). These are the two defining constants of the LZMA range coder.

6. Cross-reference with netinstall-cli (Linux)

For comparison, I also analysed the Linux netinstall-cli binary from the same release. That tool does NOT use FlashFig at all — the -s flag sends config scripts uncompressed through a proprietary UDP protocol (port 5000) as autorun.scr. FlashFig is Windows-only.

Notes

  • The --format=alone flag is important — this produces the legacy LZMA format with the 13-byte header (properties + dict size + uncompressed size), not the newer .xz container format.
  • The 0xfffe bytes after "123 " appear to be a format version/type marker, not a UTF-16 BOM despite looking like one.
  • FlashFig configs are limited to ~4000 bytes uncompressed, so the 512KB dictionary is overkill, but those are the parameters baked into the binary.
  • I have not tested this end-to-end with actual hardware yet. If anyone can validate with a Wireshark capture (compare a known .rsc input against the TFTP payload), that would confirm the format completely.

Hi @giesen I took at shot at this and have a WIP Rust tool here:

I included reference materiel that I captured on the wire from flashfig 7.22.1. Unfortunately there’s more going on here that I can’t seem to figure out. I’d love to have another set of eyes if you want to take a look at the PCAP and hexdump of the payload received on the wire for the config.rsc provided.

Follow-up to my earlier post #11 in this thread, which got the wire format
partly right and partly wrong. This post supersedes it. Headline:

We now have a complete spec of the flashfigucis wire format and a
byte-identical Linux encoder.

TL;DR:

  • The payload is NOT raw LZMA1 with a 4-byte magic prefix. The LZMA
    encoder that FlashFig uses primes its dictionary with a 106,952-byte
    corpus of RADIUS-attribute and RouterOS-config boilerplate that is
    embedded (pre-compressed) in flashfig.exe itself. The bytes that go on
    the wire are what the encoder emits while processing the user script,
    continuing from the state the primer left the encoder in.
  • Corollary: you cannot replicate FlashFig with stock LZMA1 tooling
    (xz, liblzma, Python's lzma module). Every stock encoder produces
    output that won't decompress on the RouterBOARD bootloader because the
    bootloader's decoder starts with the primer already loaded.
  • MikroTik has been using LZMA SDK 16.04 for this for a while.
    Versions 19+ of the SDK have a match-finder change that produces ~20
    extra bytes on the ~107 KB primer — we have to use 16.04 to match.

Full spec, build instructions, POC encoder, and how to verify/reproduce
against a new FlashFig version are below.

Correction to post #11

Post #11 claimed the payload layout was:

0-3    "123 "
4-5    ff fe
6      LZMA properties byte (0x04)
7-10   dict size, LE (00 00 08 00)
11-18  uncompressed size, LE
19+    LZMA compressed stream

and that the repro command was:

lzma --format=alone --lzma1=lc=4,lp=0,pb=0,dict=524288 < config.rsc > config.lzma
printf '123 \xff\xfe' | cat - config.lzma > config.flashfig

Both are wrong.

  • There is no 13-byte LZMA "alone" header between the magic and the
    compressed data. The observed bytes at offsets 7-10 of
    sjthomason's captured flashfigucis.bin are 00 01 5A 6F, not
    00 00 08 00; they are part of the 6-byte slot header described
    below.
  • writeEndMark = 0 — the wire stream has no end-of-stream marker.
  • The LZMA encoder is not called with the user script as direct input;
    it is primed first. Raw lzma --format=alone output will not decode
    on a RouterBOARD.

Correct wire format

bytes 0–3    ASCII "123 "   (0x31 0x32 0x33 0x20)
bytes 4–9    6-byte slot header
bytes 10+    LZMA1 continuation stream

6-byte slot header

offset size field
0-1 16-bit BE word Type indicator. High bit, if set, flags the word as encoded as ~type; otherwise it is type directly. Type 1 = RouterOS script, type 2 = RouterOS config. FlashFig's wire always uses the flag-encoded form (FF FE = ~1).
2 low 6 bits Flag/version byte. Observed 0x00 on the embedded primer blob, 0x04 on the user wire slot. This is not the LZMA properties byte (lc/lp/pb are fixed in the encoder, not carried in the header).
3-5 24-bit BE Uncompressed size (must be ≤ 0x80000).

LZMA1 continuation stream

The trailing bytes are not a fresh LZMA1 stream. They start mid-range-coder
state as a continuation of the dictionary priming described below. A stock
LZMA1 decoder starting from an empty dictionary will not decode them.

In the reference PCAP capture (config.rsc 346 B CRLF):

0000  31 32 33 20 FF FE 04 00 01 5A 6F 12 61 EF 15 87  …
0010  F2 E2 5C C5 6B BB 6D A9 E6 C9 5B FC 4C E0 AF 52  …
      │——magic——│—type—│ │ │—size—│ │—LZMA1 continuation—
                       │ └── flag byte (0x04)
                       └── "low-6-bits" byte (also 0x04 here; coincidence)

0x01 5A = 346, matching the 346-byte config.rsc input.

Dictionary priming

The key mechanism post #11 missed. flashfig.exe embeds a 17,650-byte
pre-compressed LZMA1 blob in its .rdata section. In v7.22.1 it lives
at VA 0x4555c0. The blob has the same wire-format structure (6-byte
slot header + LZMA1 stream) and decompresses to exactly 106,952 bytes
of RADIUS attribute dictionary and RouterOS configuration templates:

,"User-Name","User-Password","CHAP-Password","NAS-IP-Address","NAS-Port",
"Service-Type","Framed-Protocol","Framed-IP-Address","Framed-IP-Netmask",
"Framed-Routing","Filter-Id","Framed-MTU","Framed-Compression",…
/ip firewall address-list add list=test address=1.2.3.4
/routing bgp instance set default client-to-client-reflection=yes
…

At encode time FlashFig creates a single LZMA encoder instance, feeds
the decompressed primer to it first, then feeds the user's .rsc. The
output the encoder produced during primer processing is discarded. Only
the bytes emitted while processing the user script are written on the
wire. Because both runs use the same encoder instance, its match-finder
dictionary is populated with primer tokens by the time the user script
is encoded — so common MikroTik syntax (/system, set, address=,
RADIUS attributes, etc.) compresses to short match references.

The RouterBOARD bootloader does the mirror of this: it has the same
primer in ROM (otherwise nothing would decode), feeds it through its
LZMA decoder first, then continues decoding the wire stream to recover
the user script.

This is why the compression ratio on real configs is so much better than
stock LZMA1: config.rsc (346 B) → stock LZMA1 produces ~285 B; FlashFig
produces 126 B of stream (+7 B of header = 133 B on the wire).

LZMA parameters

From disassembly of flashfig.exe v7.22.1 at VA 0x43d5d4 (the dual-mode
encode/decode function, encode branch at 0x43d605+):

param value notes
level 9 LZMA SDK preset level
dictSize 0x80000 524,288 bytes
lc 4 literal context bits
lp 0 literal position bits
pb 0 position bits
fb 273 num-fast-bytes (0x111)
numHashBytes 3 combined with default btMode=1 → BT3 match finder
mc 0x80000 matchFinderCycles (very aggressive)
writeEndMark 0 no end-of-stream marker
numThreads 1

Everything else is the LZMA SDK's default after LzmaEncProps_Normalize.

Byte-identical Linux encoder

A CC0 (public-domain-dedicated) reference encoder in C, using the vendored
reference LZMA SDK 16.04, is at:

https://gitlab.com/ggiesen/mtffencoder

Verified byte-identical against sjthomason's PCAP capture:

$ ./mtffencoder ./flashfig.exe ./config.rsc /tmp/out.flashfigucis
primer: 17650 bytes embedded -> 106952 bytes text
wrote:  133 bytes -> /tmp/out.flashfigucis

$ sha256sum /tmp/out.flashfigucis ./flashfigucis.bin
53c32f29…  /tmp/out.flashfigucis
53c32f29…  ./flashfigucis.bin

Two implementation gotchas

  1. LZMA SDK version. Every SDK from 19.00 through 24.09 produces 20
    extra bytes on the primer compared to what MikroTik ships. SDK 16.04
    matches. If MikroTik later upgrades their SDK, this encoder will need
    a matching-version LZMA SDK update.

  2. The primer-to-user boundary in the output. You cannot produce
    user_wire = combined_output[primer_alone_size :]. The last ~4 bytes
    of primer-alone encoding are range-coder flush bytes that are replaced
    in combined encoding by actual user-encoded bytes. The correct way to
    find the boundary: encode primer alone, encode primer+user, find the
    first byte position where the two encodings diverge — that is where
    the user portion starts.

Reproducing this for a new FlashFig version

If MikroTik releases a new flashfig.exe with a different primer or
changes anything, here is how to find out.

1. Locate the primer

The repo includes find_primer.py which scans any flashfig.exe's
.rdata section for a 6-byte slot header whose trailing LZMA1 stream
decompresses to exactly the size the header claims:

$ python3 find_primer.py flashfig.exe
.rdata: VA 0x453000, size 41240 bytes, file offset 0x51a00

✓ primer found:
  VA                  0x4555c0
  file offset         0x53fc0
  blob size           17650 bytes
  decompressed size   106952 bytes
  Update mtffencoder.c with:
    #define PRIMER_VA            0x4555c0u
    #define PRIMER_BLOB_SIZE     17650u

Paste those two values into mtffencoder.c and rebuild.

2. Verify parameters haven't changed

Capture FlashFig's wire output for a known .rsc (use Wireshark on a
Windows or Wine session; or set up a local TFTP server and save what
FlashFig sends it). Run the encoder against the same input. If
byte-identical, you are done. If not, the codec parameters have
changed; you'll need to re-extract them by disassembling around the
encoder call site (search for calls to LzmaEnc_SetProps).

3. If the byte count diverges mid-stream

Most likely cause: MikroTik upgraded their LZMA SDK version. SDK 16.04
matches v7.22.1; SDKs 19.00+ produce 20 extra bytes on the primer. Try
each SDK version until you find one that matches.

Files

mtffencoder.c          — CC0 encoder (~400 LoC) using LZMA SDK
LzmaEnc.c/h, …         — vendored LZMA SDK 16.04 (public domain)
find_primer.py         — locates primer in any flashfig.exe (CC0)
Makefile               — `make` / `make test`
README.md              — detailed docs including build + repro
LICENSE                — CC0 1.0 Universal dedication

Build:

$ make
cc -O2 -Wall -Wextra -D_7ZIP_ST -Wno-dangling-pointer -o mtffencoder \
   mtffencoder.c LzmaEnc.c LzFind.c Alloc.c LzmaDec.c CpuArch.c

Runs on Linux, macOS, any POSIX. Single 65-70 KB static binary. No
runtime dependencies beyond libc.

Credits

  • rcy (2018) — original thread starter; solved this via
    capture-and-replay from FlashFig.
  • sjthomason — committed a reference PCAP, config.rsc, and the
    flashfigucis wire capture that made byte-level verification possible.
    The Rust encoder in that repo should be updated to drop the
    alone-format header and add primer priming; happy to help with that
    if useful.
  • normis — declined to help; respected.
  • Lots of help from an LLM in the decoding and POC work

Why not using netinstall-cli with a config file, or using the netinstall python variant.

Partially because we provision more than just MikroTiks (Cisco, Fortinet, DSL modems) with our DHCP/TFTP infrastructure, so not having to have dedicated processes/infrastucture just for MikroTik makes our lives easier.

Now that FlashFig seems to be solved, we can just put the encoded config on our TFTP server, pop the Mikrotik onto the provisioning LAN, and it gets auto-provisioned like all of our other platforms.

Netinstall is an even bigger mess of proprietary garbage with a custom UDP protocol that nothing else uses. Also, it’s Intel only (can’t run on a Raspberry Pi or a Mac). Now I wasn’t aware someone had reverse-engineered and offers a Python version (and I applaud them for that), but I’m still not likely to use it because it doesn’t fit in with how we provision things.

All that being said, having options is good, and decoding the flashfig config encoding opens up another option.

Update: mtffencoder - Mode script support, primer caching, flag-based CLI

Several rounds of polish since the initial post. Highlights below.

Quick framing first. My intention wasn't to preempt sjthomason's Rust-encoder work. mtffencoder is specifically a reference implementation - the goal is to document the format/functionality and produce byte-identical equivalence with MikroTik's own FlashFig output, so that anyone building an encoder in Rust / Python / Go / whatever has a stringent baseline to validate against.

Config vs Mode script support (-m / --mode). The FlashFig 7.22+ GUI's "Select Mode" button maps to a different wire variant than the default "Select Config" button: a leading 6-byte marker (ff fe 00 00 00 00) is prepended before the user's encoded slot. Per MikroTik's docs, Mode scripts run before the device config, can change device-mode / protected-routerboot, and are auto-removed after execution. The encoder now emits both variants - pass -m for a Mode script, omit it for a regular Config script. The LZMA payload itself is byte-identical between the two; only the framing differs. Verified against FlashFig 7.21.1 - pre-7.22 has no "Select Mode" button on the GUI and only ever produces the single-slot Config variant.

Footnote on the "magic header" arc. Post #11 framed the leading bytes as a Mikrotik magic header / format marker. Post #13 retracted that and recast them as a parsed 6-byte slot header (type / flags / 24-bit size). Post #13 was right - they really are slot-header fields, not a flat magic. But the Mode-script behavior is a fun footnote: when the GUI is in mode-button mode, that slot header is always emitted with type=1, byte2=0, size=0, so the actual on-the-wire bytes are literally the constant sequence ff fe 00 00 00 00. So in a Mode-script capture it does function as a marker - it just also happens to be a structurally well-formed empty slot header.

Primer caching (-x extract, -p use cached). Extract the dictionary primer from flashfig.exe once, save to a file, then encode subsequent runs without flashfig.exe being present. Cache file has an MTFFPRIM magic header so accidentally pointing -p at the wrong file fails fast (clear error) instead of silently producing a broken payload that only gets caught at router-boot time.

Flag-based CLI. Replaces the original positional-args form. mtffencoder --help for the full option list. Default is silent on success; -v adds progress output.

Build / install / CI polish. The repo now has:

  • make install honouring PREFIX / DESTDIR (drops mtffencoder and mtffencoder-find-primer into bin/, README/LICENSE into share/doc/) - packager-friendly out of the box.
  • GitLab CI on every push: clang-format check, clang-tidy lint, ruff for the Python helper, 25 CLI smoke tests (no proprietary inputs needed), and the byte-identical regression test gated on a CI variable pointing at a known-good flashfig.exe.
  • Auto-detection of GCC vs Clang in the Makefile, so macOS and BSD builds don't need special-casing. Should build cleanly on macOS (Clang autodetected) and BSDs (use gmake), though I've only personally verified on Linux/GCC.
  • Tagged releases (v0.X.Y) auto-publish a source tarball to the GitLab Releases page

On byte-identity. The default-mode (no -m) output is byte-identical to MikroTik's v7.22.1 reference capture, verified in CI on every push. Whether the router actually requires byte-identical encoding, or accepts a broader class of valid LZMA1 streams that decode to the same .rsc through the same primed dictionary, is an open question I haven't tested. Byte-identity was just the easiest, most rigorous property to test mechanically against the reference capture; if anyone has tried a non-byte-identical encoding against a real RouterBOARD, I'd be interested to hear the result.

License change - CC0 → Blue Oak Model License 1.0.0. Same intent as before - do what you want with it - but Blue Oak adds an explicit patent grant that CC0 doesn't carry, which have caused some orgs to reject CC0 for software. Blue Oak (drafted by Blue Oak Model License 1.0.0 ) is functionally just as permissive while explicitly waiving patent claims too. The vendored LZMA SDK files remain separately Igor Pavlov's public-domain dedication. The encoder doesn't bundle MikroTik's primer — it extracts at runtime from your own copy of flashfig.exe, same as before.

Repo: https://gitlab.com/ggiesen/mtffencoder

Update: mtffencoder v0.2.0 - byte2 algorithm cracked, full byte-identical match across an expanded corpus

Quick correction and progress update following from posts #11/#13/#16 in this thread.

What changed

In post #13 I described the wire-format slot header as having a 6-byte structure with a "byte 2 (flags / version field)" that I had observed taking values 0x00 (on the embedded primer blob) and 0x04 (on the user wire slot in the reference capture). I treated 0x04 as a fixed value for the user slot and the encoder hardcoded it as such.

That was wrong. The reference PCAP just happened to land on 0x04 and the bug only became visible after I built a wider corpus.

byte2 is the trailing-divergence count between two LZMA encodings of the user data:

P = bytes produced by encode(primer) alone, writeEndMark=0
B = first byte position where encode(primer) and encode(primer || user) diverge
byte2 = P - B

In other words, byte2 is the count of trailing range-coder flush bytes that primer-alone would emit but primer-plus-user replaces with real user-encoded data.

How I figured it out

The investigation went through a few wrong turns and is worth recording in case anyone hits something similar.

1. Build a varied corpus. sjthomason's reference capture had byte2 = 0x04 and the encoder hardcoded 0x04, so byte-identity passed. Hand-crafting .rsc inputs at varied sizes (40 B, ~100 B, ~200 B, ~300 B, 1 K, 10 K, 100 K, 256 K) and capturing FlashFig output against each immediately surfaced byte2 = 0x09 for some inputs and 0x04 for others. Lesson: one-capture regression tests hide content-specific encoder bugs.

2. Rule out simple hypotheses. Checked input length and length-mod-N, CRC32 of input, sum/xor of stream bytes, first/last byte of input/stream. None matched. Then noticed a strong correlation: every #-prefixed input gave 0x04, every /-prefixed input gave 0x09. Wrong conclusion: "byte2 is a first-byte lookup."

3. Instrument the encoder. Added a small getter to the vendored LZMA SDK to read the encoder's terminal _state field (hypothesis: byte2 is the LZMA state machine value at termination, since values 4 and 9 are both valid LZMA states 0..11). Ran on the corpus. State spanned {1, 8, 10} for inputs that all had byte2 = 0x04. Hypothesis cleanly disproved.

4. Static analysis with a focused query. Strong empirical correlation pointing to a non-encoder mechanism. Searched flashfig.exe (with capstone + pefile in Python) for the inverse of the slot-header parser pattern (a not <reg> followed by a memory store derived from that register). Hit at VA 0x43d8fc, the slot-header build function. The byte2 store at 0x43d847..0x43d863 is computed via a byte-by-byte compare loop:

0x43d847: cmp [length], match_count
0x43d84a: jae 0x43d858            ; if length >= match_count
0x43d84c: byte2 = 0
0x43d858: byte2 = length - match_count

i.e. byte2 = trailing-difference length between two byte arrays. The two arrays are the original input bytes and a re-encoded version produced by a recursive call. FlashFig is doing a self-validation round-trip: encode, parse-back, re-encode, store the trailing mismatch in byte2.

5. Connect it to existing code. mtffencoder already computed B to slice the wire stream out of the primer+user encoding. byte2 falls out of that for free as primer_alone_size - B. One-line fix.

Other findings worth knowing

While building the corpus we hit FlashFig's actual encode ceiling, which turns out to be much smaller than the wire format would suggest:

  • The 24-bit slot-header size field permits up to 16 MB.
  • The LZMA dictionary is 524,288 bytes which I had assumed was the cap.
  • FlashFig's GUI actually bails on inputs around ~407 KB with "Compressed = 0 B" and serves only the 4-byte magic over TFTP. The cap is non-monotonic in input size: 404 KB encoded fine, 407.3 KB and 407.5 KB failed, 407.9 KB encoded fine. That points at a fixed internal output buffer that interacts with how compressible the specific input is.
  • Practical advice: if you're round-trip-validating an encoder against FlashFig's output, keep test inputs under ~350 KB for safety. My corpus uses 256 KiB as the largest test case.

Status

  • mtffencoder v0.2.0 produces byte-identical output to FlashFig 7.21.1 and 7.22.2 for every input we have a capture for (18 captures across 8 inputs, 40 B to 256 KiB, both Config and Mode buttons).
  • CI runs the corpus regression test on every push.
  • Tag and source tarball published on GitLab.

Repo: https://gitlab.com/ggiesen/mtffencoder

Some fine print:

  • byte2 is now correctly computed for everything we have empirical data for. Nothing rules out the existence of input shapes that produce a byte2 we haven't seen, but the algorithm is grounded in disassembly so it should generalize.
  • I have only ever tested against FlashFig 7.21.1 and 7.22.2 with a single RouterBOARD model (RBM33G) on the receive side. If anyone is in a position to test against a different model, I would be interested in the result.
  • I haven’t tested sending these from a non-Flashfig sender yet, so it’s entirely possible there are more hidden mechanisms at work, although we should be getting very nearly to the end if we’re not there already.

One thing to have "your robots" take a look at is my @tikoci/quickchr project on GitHub. This uses QEMU to bring up a CHR via CLI (or TUI wizard or NodeJS API callers). quickchr allows you set device-mode to allow routerboard=yes, and you be able to set the boot mode to flash-boot. This allow more direct testing, since you agent can create/destroy CHR machines as needed for testing. There is an exec command that will run the need /system/routerboard/set boot-mode=flash-boot; /system/reboot (I think that's the commands but didn't check). See npx @tikoci/quickchr --help for high-level overview.

And to your limited versions problem, quickchr has an option to create any version you want... so if you structure a test so it work to "flashfig" using your code, against the CHR provisioned by quickchr (and updated with exec to put into flash-boot). Your agent can the just loop with quickchr to do the few commands needs to bring up, device-mode routerboard=yes, exec ... boot-mode=flash-boot, and test your flashfig works... the repeat.

The only tricky part here with tikoci/quickchr is you need a TAP setup on linux (or use vmnet_socket on Mac) to get a "real" interface for it. Or potentially use OpenVPN TAP driver, although that part is untested on Windows... Issue is unlike a lot of things.... flashfig does need L2 access while quickchr's default will use 91xx as starting ports that is merely forwards via SLiRP/"host only" networking (which is good for config/commands/etc but not netinstall/flashfig things).

You'd like what to point your agent at my 3 main "AI projects":
GitHub - tikoci/quickchr: `quickchr` - Fastest path to MikroTik RouterOS CHR instance for testing and scripts · GitHub (CLI tool for bring up CHRs for testings with a few commands)
GitHub - tikoci/routeros-skills: Custom instruction SKILL.md for MikroTik RouterOS v7 · GitHub (backing knowledge about QEMU and CHR so agent does not have to "fly blind" on what's going on)
GitHub - tikoci/rosetta: MCP Server with RouterOS docs + commands + products + changelogs, using SQLite-as-RAG, sourced from MikroTik · GitHub (RouterOS docs as MCP server, although you likely have all the [limited] docs you'd need, it would help to figure out the right command to use since rosetta also know the entire command tree)

Also, most RouterOS things use some set of what I call "Nova IDs" that generally map to numbers to RouterOS actions/data/protocols. So I'd imagine flashfig follows that, so if it knew slightly more from the MarginResearch or other projects like GitHub - subixonfire/winbox-terminal-protocol: Description: Pure Python implementation of the MikroTik WinBox terminal protocol — encrypted CLI access to RouterOS devices over TCP port 8291 · GitHub and GitHub - german77/TheDudeToHuman: Converts "The Dude" database from Mikrotik into something more usable than binary blobs · GitHub on GitHub... Your agents may have a better time unwinding the "unknown" bit since the OTHER RouterOS thing may know more about the general structure.

Finally, quickchr can bring up both ARM64 and X86 using QEMU. If you on X86, ARM64 is only just "notable slower"... but ARM64 running X86, that be "EXTREMELY SLOW, like >minute to boot, etc.). So you have 2 dimensions you can test version and arch. Your RBM33G cover ARM, so this fill in ARM64 (using ARM64 CHR which shares same kernel/etc as hardware, so it's close to the modern devices I'd imagine).

Not trying to "pitch" my tools. But I do suspect that be useful, maybe not, that be good feedback too. But it's this kinda of work I build them to do. Your project is just one more angle previously unexplored parts of "RouterOS Internals".

More food for thought in your work here.

Yeah, I’d definitely considered using CHR for testing, but I wasn’t sure if it supported Flashfig given it’s not a RouterBoard / is a virtual appliance and Flashfig may be of limited utility.

That being said, once I have a few more cycles I may poke around at it, as the hardware Flashfig testing cycle is quite tiresome. If CHR works, it may also open up some options for doing testing right in pipeline/CI/CD. Thanks very much for the suggestion, I’ll check out your tools.

The RBM33G is actually MMIPS, but I do also have some other hardware lying around I can test with when I get some cycles… Hex (MMIPS), Hex S (MMIPS), RB5009UG+S+IN (ARM64), CRS310-8G+2S+IN (ARM32), RB750GL (MIPSBE).

At this point I’d have been more than happy if MikroTik had just used standard protocols like BOOTP/DHCP & TFTP than the god awful concoction they’ve come up with, but it’s definitely been interesting diving into the internals of how Flashfig works.

Nope. I was fooled. I'd for sure seen the /system/routerboard in ARM64 CHR... however I never did a print, I just "noted" it had it and assume it be same as ARM64 hardware (since it does share same kernel).

But, it just shows "routerboard: no" when you print, and set has nothing to set. Too bad. That would made this much easier.