Skip to content

Command translation on non API transports

Danik edited this page Jun 16, 2026 · 1 revision

Command translation on non-API transports

Every transport in tik4net is used through the same ITikConnection / ITikCommand surface. You build a command against an API-style path (/ip/address/print) with API field names (src-address) and call one of the ExecuteXxx methods — exactly as you would on the binary API.

On the binary Api/ApiSsl transport those calls go straight onto the wire as the native length-prefixed sentence protocol. On every other transport (Telnet, SSH, MAC-Telnet, WinBox CLI, WinBox Native, REST) the same call has to be translated into whatever that transport actually speaks — a CLI line, an M2 message, or an HTTP request — and the textual/binary reply parsed back into the !re/!done sentence model the O/R mapper expects.

This page explains what gets translated, into what, and in which layer.

TL;DR — ExecuteXxx → a verb (print/add/set/…) → one of three transport hooks (RunPrint / RunAdd / RunNonQuery) → a per-transport builder turns the path + parameters into the wire form → a per-transport parser turns the reply back into sentences. Your code never sees the difference; only the capabilities differ (no streaming / tagging / raw sentences off the binary API).


The layers

            your code: conn.CreateCommand("/ip/address/print").ExecuteList()
                                   │
   ┌───────────────────────────────┼─────────────────────────────────────────────┐
   │ ITikCommand  (TikGenericCommand)                                              │
   │   • ExecuteList / ExecuteScalar / ExecuteSingleRow → read   → RunPrint        │
   │   • ExecuteNonQuery                                 → write  → RunNonQuery     │
   │   • (add verb / ExecuteScalar on add)              → insert → RunAdd          │
   │   • ExecuteAsync / LoadAsync / LoadListenAsync     → monitor → RunMonitorAsync │
   │   • verb is the LAST path segment (print/add/set/remove/move/…)               │
   └───────────────────────────────┼─────────────────────────────────────────────┘
                                   │  (transport-neutral: path + ITikCommandParameter list)
   ┌───────────────────────────────┼─────────────────────────────────────────────┐
   │ TikCommandConnectionBase  — common plumbing for all non-API transports        │
   │   abstract RunPrint(descriptor) : IList<TikRecordSentence>                     │
   │   abstract RunAdd(descriptor)   : string (new .id)                             │
   │   abstract RunNonQuery(descriptor)                                             │
   └───────────────────────────────┼─────────────────────────────────────────────┘
              ┌────────────────────┼─────────────────────────┐
              ▼                    ▼                          ▼
   ┌────────────────────┐ ┌────────────────────┐  ┌───────────────────────────┐
   │ CliConnectionBase  │ │ WinboxNative       │  │ RestConnection            │
   │ Telnet / SSH /     │ │ Connection         │  │ (own builder/parser)      │
   │ MAC-Telnet /       │ │                    │  │                           │
   │ WinBox CLI(+MAC)   │ │ M2 getall/set/…    │  │ GET/POST/PATCH/DELETE     │
   │                    │ │                    │  │                           │
   │ CliCommandBuilder  │ │ WinboxHandlerMap   │  │ RestRequestBuilder        │
   │  → CLI text        │ │  path → [maj,min]  │  │  → URL + JSON             │
   │ CliOutputParser    │ │ WinboxFieldResolver│  │  System.Text.Json parse   │
   │  text → sentences  │ │  name ↔ M2 key     │  │                           │
   └────────────────────┘ └────────────────────┘  └───────────────────────────┘
              │                    │                          │
        terminal / PTY      encrypted M2 (8291/20561)     HTTP(S)

The transport-neutral half (top two boxes) is identical for everyone: command parsing, verb detection, parameter normalization, the Run* contract, diagnostics (OnReadRow/OnWriteRow) and Safe Mode plumbing all live in TikGenericCommand + TikCommandConnectionBase. Only the bottom box — build + parse — is transport-specific.


Step 1 — which ExecuteXxx calls which hook

ITikCommand is implemented once for all non-API transports by TikGenericCommand. It looks at the verb (the last segment of the command path) and dispatches to one of the three hooks:

ExecuteXxx Verb Hook Returns
ExecuteList() / ExecuteList(proplist) print (or none) RunPrint all matching rows
ExecuteSingleRow() / …OrDefault() print RunPrint exactly one / null
ExecuteScalar() / …OrDefault() print RunPrint one field of one row
ExecuteScalar() on an …/add path add RunAdd the new .id
ExecuteNonQuery() add RunAdd
ExecuteNonQuery() set/remove/unset/move/enable/disable RunNonQuery
ExecuteNonQuery() action verb (run, reboot, …) RunNonQuery
ExecuteAsync / LoadAsync / LoadListenAsync print/listen/monitor RunMonitorAsync (if supported) rows via callback
ExecuteListWithDuration (streaming) throws …CapabilityNotSupportedException

Two normalizations happen before the hook is called, identical on every transport:

  • Multi-line command — a command text like "/interface/print\n?type=ether" is split into the path plus parsed ?(Filter)/=(NameValue) parameters (NormalizeMultilineCommand).
  • Read parameter coercion — for reads, bare Default parameters are promoted to Filter (?name=value) form, because on a read they mean "filter by", not "set" (ResolveParamsForRead).

proplist (field trimming) is ignored off the binary API — the CLI/native/REST reads always return the full field set, and the O/R mapper just picks the fields it needs.


Step 2a — CLI family (Telnet · SSH · MAC-Telnet · WinBox CLI · WinBox CLI/MAC)

All five CLI transports share CliConnectionBase; they differ only in the socket underneath. The translation is done by CliCommandBuilder (build) and CliOutputParser (parse).

Path translation

API path segments become CLI words — the slashes turn into spaces, the last segment is the verb:

/ip/address/print            →  /ip address print
/interface/ethernet/monitor  →  /interface ethernet monitor

Read (RunPrint)

A read is wrapped in :put [ … as-value ] so RouterOS materialises a machine-readable line (a bare print as-value prints nothing to a terminal):

ExecuteList() on /ip/address with ?disabled=false
   →  :put [/ip address print as-value where disabled=false]

Reply (one line, records concatenated, each starting at .id=):

.id=*1;address=192.168.88.1/24;network=192.168.88.0;interface=ether1;disabled=false;.id=*2;address=…

CliOutputParser.ParseAsValue splits this back into TikRecordSentence objects (one per .id=), which are returned to the O/R mapper as !re sentences.

Where-clause translation (Filter parameters → RouterOS where):

Parameter (API filter) CLI where
?chain=input where chain=input
?chain=!input (negation) where chain!=input
?>count=100 where count>100
?<count=100 where count<100
?~comment=eth (regex) where comment~eth
multiple filters joined with &&

Values that contain characters the CLI parser treats as operators (/ in 192.168.1.1/24, : in MACs) are quoted in the where clause (QuoteForWhere); name=value arguments with spaces/;/# are quoted for add/set (QuoteIfNeeded).

Live counters — entities flagged with .cli-stats are read with two queries (print detail as-value for config + print stats as-value for byte/packet counters) and merged by .id, because RouterOS exposes those columns only under the stats modifier.

Insert (RunAdd)

/ip/address/add  =address=10.0.0.1/24  =interface=ether1
   →  :put [/ip address add address=10.0.0.1/24 interface=ether1]

The :put [ … ] wrapper makes RouterOS echo the new record's .id (*N), which is returned as the scalar result.

Update / delete / reorder (RunNonQuery)

These select the target by .id with a [find …] expression:

/ip/address/set  =.id=*1  =comment=lan
   →  /ip address set [find .id=*1] comment=lan

/ip/address/remove  =.id=*1
   →  /ip address remove [find .id=*1]

A convenience: if you pass a name instead of a *N handle, the CLI builder widens the selector to [find .id=X or name=X], so set .id=ether1 … resolves a named record (the binary API/native accept a name directly; this bridges the CLI gap).

What the transport does vs. what the core does

  • Core (CliCommandBuilder/CliOutputParser) is transport-agnostic: it only produces/consumes clean text (data lines key=value;…).
  • The transport (Telnet/SSH/WinBox CLI/MAC-Telnet) is responsible for the terminal mechanics it sees before the core: stripping ANSI/VT100 escape sequences (VtStripper), trimming command echo and the [user@router] > prompt, and (on a paged PTY) suppressing paging. The core never sees a control byte.
  • Errors — the reply text is scanned by CliErrorParser, which maps no such item, already have such…, no such command, failure: etc. to the same tik4net exception types the binary API raises (TikNoSuchItemException, TikAlreadyHaveSuchItemException, …).

Async / listen / monitor on CLI

A terminal has no server push, so ExecuteAsync/LoadAsync/LoadListenAsync are emulated by polling (that's why CLI reports Listen but not Streaming):

  • /path/print (LoadAsync) — runs the read once on a worker, emits rows, completes.
  • /path/listen — re-reads the table on a timer and diffs snapshots by .id; an added/changed row fires your callback, a vanished .id fires a synthetic delete.
  • a monitor verb (monitor-traffic, profile, ping, …) — re-issues a one-shot :put [… once as-value] every ~500 ms. Interactive-only verbs (torch) are rejected with guidance to use the binary API.

Step 2b — WinBox Native (M2)

WinBox Native skips the terminal entirely and issues the binary M2 key–value messages WinBox itself uses. The translation problem is different: the M2 protocol addresses things by number, not text. See WinBox-Native-connection for the full mapping story; in brief:

Translation From To Source
Path → handler /ip/firewall/filter [20, 3] live .jg menu tree + shipped aliases + PathOverride
Field name ↔ key src-address 0x… key live .jg field table + seeds + aliases + FieldOverride
Verb → M2 command print/set/add/remove/move getall/set/add/remove/move cmd hard-coded protocol constants
Value encode/decode 192.168.1.1/24, AA:BB:…, enum u32 addr+mask, 6 raw bytes, numeric index/ref-id .jg field type (WinboxFieldResolver)
  • Reads (RunPrint) call M2 getall (or get-singleton for item windows), decode each record's numeric keys back to API field names, then apply any ?name=value Filter parameters in-memory (RouterOS-side filtering is not used) via the same postfix query stack the CLI async path uses.
  • Writes (RunNonQuery/RunAdd) resolve the target .id (a *HEX handle, or a friendly name resolved via getall) and encode each field by its .jg type.
  • Errors — the M2 error code + router text are mapped to the same exception types as the API/CLI transports.
  • Async/monitors.jg query windows (torch, profile, monitor-traffic, ping) are polled start → poll → cancel on a worker; listen is poll+diff, exactly like CLI.

The handler/field numbers are never hard-coded — they are read live from the router's version-matched .jg catalog and cached. Only the stable English text bridge (API name ↔ GUI label normalizer + alias dictionary) ships in the library.


Step 2c — REST

The Rest/RestSsl transport (RestConnection, RouterOS 7.1+) maps the verb to an HTTP method and the path to a URL, with System.Text.Json for the body (RestRequestBuilder):

Verb HTTP
print GET /rest/ip/address (filters → query string)
add POST /rest/ip/address (JSON body)
set PATCH /rest/ip/address/*1
remove DELETE /rest/ip/address/*1

REST is stateless request/reply: it reports Crud only — no Listen, no Streaming, no Safe Mode.


What this means for you

  • Same code, any transport. LoadAll<T>(), Save, Delete, ExecuteList, ExecuteScalar, ExecuteNonQuery behave identically; only performance and the feature set differ.
  • Check capabilities, not transport type. Streaming monitors, tagged async and raw sentences exist only on the binary API. Guard with connection.Supports(TikConnectionCapability.Streaming) (see Connection types & capabilities).
  • Errors are uniform. Every transport raises the same TikNoSuchItemException / TikAlreadyHaveSuchItemException / TikNoSuchCommandException / … types, so catch blocks port across transports.
  • Diagnostics show the wire form. Attach OnWriteRow/OnReadRow (or set DebugEnabled) to see the translated CLI line / M2 message describe / HTTP request that actually went out — handy when a mapping is off. See Communication debugging & testing.

Escape hatches

Two features build directly on the translation model above. Both ship on the 4.x branch.

Raw command pass-through (CreateRawCommand)

For commands the structured mapping doesn't cover (or one-offs you'd rather type verbatim), the capability-gated factory CreateRawCommand(raw) sends a raw payload in the transport's own native dialect — bypassing the builder/mapper — while keeping the normal ITikCommand / ITikReSentence model. Raw-ness is a property of the command (an internal command kind), not a connection mode:

// payload format depends on the transport (see table)
ITikCommand cmd = conn.CreateRawCommand("/interface print stats as-value");

foreach (ITikReSentence re in cmd.ExecuteList())   // send → parse → sentences
    Console.WriteLine(re.GetResponseField("name"));

string export = conn.CreateRawCommand("/export").ExecuteScalar();   // free-form → raw text

// wrapAsValue:true wraps a bare CLI print in ':put [ … as-value]' so it yields parseable rows
foreach (var re in conn.CreateRawCommand("/ip address print", wrapAsValue: true).ExecuteList()) { /* … */ }
Transport Raw payload ExecuteList returns Supported
Api / ApiSsl API sentence, \n-separated words (/interface/print\n?type=ether) real !re sentences (lossless)
CLI family (Telnet/Ssh/MacTelnet/WinboxCli/WinboxCliMac) verbatim CLI line as-value parsed to sentences
Rest / RestSsl — (raw JSON possible but low value; verb + path still needed) ❌ throws CapabilityNotSupportedException
WinboxNative / WinboxNativeMac — (M2 is handler + cmd + numeric keys, not a string) ❌ throws CapabilityNotSupportedException

The structured ExecuteXxx path stays the default; this is an explicit escape hatch that reuses the existing command/sentence abstraction (no parallel string-only API). It is gated by the RawCommand capability, so REST and WinBox Native — where "raw" would mean a numeric M2 message, not a string — don't declare it and fail closed; use the WinBox CLI transport for raw access over that same encrypted channel. Parameters added to a raw command are ignored — the whole payload lives in the command text. (Note: the binary API already exposes a raw path via CallCommandSync; the factory is just the unified ITikCommand-shaped front for it, so on the API a raw command needs no flag.)

Addressing WinBox Native by GUI names

WinBox Native derives API field names from the GUI labels in the .jg (lower-case, spaces/_-, abbreviation dots dropped). With UseGuiNames you can also address paths/fields by the GUI name you see in WinBox — a label copied straight from the GUI resolves without knowing the exact API spelling:

var conn = (WinboxNativeConnection)ConnectionFactory.CreateConnection(TikConnectionType.WinboxNative);
conn.UseGuiNames = true;          // opt-in; default is strict API-name resolution
conn.Open(HOST, USER, PASS);

conn.CreateCommandAndParameters("/IP/Firewall/Filter/set",
        ".id", id, "Src._Address", "10.0.0.0/24")   // GUI-style path + field names
    .ExecuteNonQuery();

The input name is matched verbatim first; only if that misses is the existing label normalizer applied (so spaces, underscores, abbreviation dots and case all fold to the API form). It is opt-in per connection so it never clashes with strict resolution; session FieldOverride/PathOverride are checked first and always win; and decoded output always uses canonical API names regardless of the flag. Because the resolver dictionaries are already case-insensitive, what UseGuiNames really adds is folding the WinBox separators (space / _-, . dropped) on the input side.

Both features are listed on the 4.x roadmap.


See also

Clone this wiki locally