-
Notifications
You must be signed in to change notification settings - Fork 98
One task on every transport and API level
This page holds one task fixed and varies the two axes around it: the API level you write against (low-level · ADO.NET-like · O/R mapper) and the transport the connection runs over (all 11 of them).
Each of the three sections below carries one tab per transport, and 29 of the 33 tabs are a complete,
runnable program. Take the tab you need, paste it into a fresh console project, set
Host/User/Password, and run it — nothing is elided, and each program's header comment states what it
needs on the router, what goes in, what comes out, and what to watch out for on that particular transport.
The four tabs that are not programs are all in the low-level section: Rest, RestSsl, WinboxNative and
WinboxNativeMac have no command language to write a raw command in, so that level does not exist there.
Those tabs say why, and point at the two levels that do work on the same connection.
GitHub's wiki has no tab control, so each "tab" is a collapsible block — click a transport to open it.
Looking for create/read/update/delete written out per API level instead? CRUD examples for all APIs covers the four operations on
/ip/address.
Find the interface whose comment is
managed-by-tik4net, keep its.id, and write a new comment back — addressing the row by that.id.
It is deliberately not "find and update in one call". Storing the .id between the two steps is what real
code does when it reads a row, decides something about it, and writes back, and it is the point where the
three API levels visibly differ: the low-level API hands you a .id string, the ADO.NET-like API hands you
a row you take it from, and the O/R mapper hands you an object that carries it, so you never mention
.id at all.
.id is what every write verb addresses. It is assigned by the router, is unique within a menu, and stays
valid while the row exists — a name or a comment can be changed by someone else between your two calls.
The samples assume one interface carries that comment. SingleOrDefault / LoadSingleOrDefault are chosen
for that reason: they return null for "no such row" and throw if the filter matched more than one, which
is usually what you want when a marker comment is supposed to be unique.
Two things every program needs, on top of the per-transport prerequisites in its header comment:
# 1 — the row the samples look for
/interface set [find default-name=ether1] comment="managed-by-tik4net"
# 2 — a user whose group carries "read" and "write", plus the policy for the transport
# you are using. The per-transport policy is stated in each program; getting it wrong
# does NOT always say so — see the table below.
/user/group print
Interface is used because every router has one and a comment is free to set and revert. Undo with
/interface set [find default-name=ether1] comment="".
Which policy each transport needs — measured on RouterOS 7.24 by logging in with a user that had only
read,write,test,local, then adding one policy at a time:
| Transport | Policy | What a missing policy looks like |
|---|---|---|
Api, ApiSsl
|
api |
trap: std failure: not allowed (9)
|
Rest, RestSsl
|
rest-api |
HTTP 401 Unauthorized — reads like a wrong password |
Telnet, MacTelnet
|
telnet |
Login failed, incorrect username or password — also reads like a wrong password
|
Ssh |
ssh |
Permission denied (password) |
WinboxCli, WinboxCliMac, WinboxNative, WinboxNativeMac
|
winbox |
refused: std failure: not allowed (9)
|
Note the third row: MAC-Telnet authenticates against the telnet policy, not a MAC-specific one, and
three of the five messages are indistinguishable from a typo in the password. If a login fails on a
password you are sure of, check the policy before checking the password.
the transport's own command language in, raw sentences out
This level is deliberately not transport-portable, and that is what makes it useful. What you write is
sent to the router unchanged, so anything the router understands is reachable — scripting, /export, a menu
tik4net has no entity for. "Unchanged" is the catch: the command has to already be in that transport's own
language, and four transports have no such language at all.
-
Api/ApiSsl— API sentence words:/interface/print,?comment=…,=.id=…. -
The CLI family (
Telnet·Ssh·MacTelnet·WinboxCli·WinboxCliMac) — a RouterOS CLI line, the text you would type at a terminal. Nothing is translated: the API words above mean nothing here, and vice versa. -
Rest/RestSsl,WinboxNative/WinboxNativeMac— an HTTP request and a numeric M2 message are shapes, not languages a caller can write. These four declare noRawCommand, so this level does not exist on them. Their tabs below say so and point at the two levels that do work there.
CallCommandSync lives on ITikRawSentenceConnection, not on ITikConnection, and there is no
extension method that puts it there — a call that compiles on all eleven transports and works on seven
would be worse than one the compiler rejects. Ask the transport's own factory for the connection and the
method is simply present: setup.CreateApiConnection() returns ITikApiConnection,
setup.CreateTelnetConnection() returns ITikCliConnection. The rules of this level are collected on
Low-level API.
Seven complete, copy-and-run programs and four tabs explaining why there is nothing to run — pick your tab:
Api — binary API, TCP 8728
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=api] # TCP 8728
// /user/group set <group> policy=api,read,write,... # the 'api' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection —
// CreateApiConnection() returns ITikApiConnection, which has it.
// The rows are API sentence words and are sent unchanged, so this program does NOT
// port to a CLI transport — see the Telnet tab for the same task in CLI text.
// A refused command arrives as a !trap SENTENCE here and does NOT throw. This is the
// only family that behaves that way; the CLI transports throw instead.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Api;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
// CreateApiConnection() returns ITikApiConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikApiConnection connection = setup.CreateApiConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker);
// The result is SENTENCES, not rows: only !re sentences carry data, and the
// trailing !done is a sentence too — so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" word, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
var reply = connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
// On THIS transport a refusal is a !trap SENTENCE — the call does not throw,
// so the reply has to be inspected. The CLI transports throw instead.
var trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
if (trap != null)
{
Console.WriteLine("update refused: " + trap.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: Api.
ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /certificate add name=server common-name=<router> ; /certificate sign server
// /ip/service set api-ssl certificate=server disabled=no # TCP 8729
// /user/group set <group> policy=api,read,write,... # same 'api' policy as Api
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection —
// CreateApiSslConnection() returns ITikApiConnection, which has it.
// Use CertificateValidationCallback instead of AllowInvalidCertificate in production -
// the flag disables validation entirely.
// A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Api;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
// default since 4.0, and without this the open fails with TikConnectionSSLErrorException
// ("The remote certificate was rejected...") before authentication is even attempted.
AllowInvalidCertificate = true,
};
// CreateApiSslConnection() returns ITikApiConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikApiConnection connection = setup.CreateApiSslConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker);
// The result is SENTENCES, not rows: only !re sentences carry data, and the
// trailing !done is a sentence too — so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" word, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
var reply = connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
// On THIS transport a refusal is a !trap SENTENCE — the call does not throw,
// so the reply has to be inspected. The CLI transports throw instead.
var trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
if (trap != null)
{
Console.WriteLine("update refused: " + trap.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: ApiSsl.
Rest — HTTP REST API, TCP 80 (RouterOS 7.1+) · this level does not exist here
There is no low-level program for REST, because REST has no command language to write one in.
This level sends what you hand it unchanged, so it needs a dialect. A REST call is an HTTP request — a
method, a URL path built from the command path, and a JSON body — assembled by the transport from the
command you asked for. There is no line of text a caller could pass through verbatim, so RestConnection
implements neither ITikRawSentenceConnection nor
RawCommand:
using tik4net;
using tik4net.Rest;
var setup = new TikConnectionSetup(Host, User, Password);
using ITikRestConnection connection = setup.CreateRestConnection();
connection.Supports(TikConnectionCapability.RawCommand); // false
// connection.CallCommandSync(...); // does not compile — no such method
connection.CreateRawCommand("/interface print"); // throws TikConnectionCapabilityNotSupportedExceptionUse one of the two levels above instead. Both build the request rather than passing one through, and
both run on REST unchanged: the same task is in the ADO.NET-like Rest tab and the
O/R mapper Rest tab.
Transport reference: Rest.
RestSsl — HTTP REST API over TLS, TCP 443 · this level does not exist here
Same as Rest: no command language, so no low-level program. TLS changes how the request travels,
not what it is — an HTTP method, a path and a JSON body, with nothing for a caller to write verbatim.
RestSsl therefore reports no
RawCommand either, and CallCommandSync is not on
ITikRestConnection.
using tik4net;
using tik4net.Rest;
var setup = new TikConnectionSetup(Host, User, Password) { AllowInvalidCertificate = true };
using ITikRestConnection connection = setup.CreateRestSslConnection();
connection.Supports(TikConnectionCapability.RawCommand); // falseUse one of the two levels above instead: the ADO.NET-like RestSsl tab and the
O/R mapper RestSsl tab do this task over the same connection.
Transport reference: RestSsl.
Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=telnet] # TCP 23
// /user/group set <group> policy=telnet,read,write,... # the 'telnet' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
// network you do not control.
// A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
// or password" — it looks like a wrong password but is not (measured).
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateTelnetConnection()
// returns ITikCliConnection, which has it.
// The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
// "=.id=...") are NOT translated here. Several rows are joined with one space, so the
// line may be written in one piece or split.
// A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
// script context, so a bare 'print as-value' at a terminal comes back as an empty line.
// 'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
// row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
// mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
// 'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
// A refusal is classified from the output TEXT, because raw mode does not know which verb
// was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
// a way the classifier does not recognise can pass as success. The two levels above are
// verb-aware and do not have that gap.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Telnet;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
// CreateTelnetConnection() returns ITikCliConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikCliConnection connection = setup.CreateTelnetConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
// ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
// terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
// the value in 'where' is quoted so a marker with a space or a '/' still parses.
var found = connection.CallCommandSync(
":put [/interface print detail as-value where comment=\"" + Marker + "\"]");
// as-value output comes back as one !re sentence per record, closed by a !done —
// so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" field, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// The CLI addresses the row by that same .id, written as a bare selector.
// A write that succeeds prints nothing at all; a refusal arrives as an exception.
try
{
connection.CallCommandSync(
"/interface set " + id + " comment=\"" + NewComment + "\"");
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: Telnet.
Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=ssh] # TCP 22
// /user/group set <group> policy=ssh,read,write,... # the 'ssh' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// The ONLY transport that ships in its own NuGet package (tik4net.ssh, which pulls in
// Renci.SshNet). CreateSshConnection() lives in that package and needs no registration;
// Tik4NetSsh.Register() is only for the ConnectionFactory route,
// setup.Create(TikConnectionType.Ssh), which throws NotImplementedException without it.
// A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateSshConnection()
// returns ITikCliConnection, which has it.
// The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
// "=.id=...") are NOT translated here. Several rows are joined with one space, so the
// line may be written in one piece or split.
// A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
// script context, so a bare 'print as-value' at a terminal comes back as an empty line.
// 'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
// row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
// mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
// 'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
// A refusal is classified from the output TEXT, because raw mode does not know which verb
// was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
// a way the classifier does not recognise can pass as success. The two levels above are
// verb-aware and do not have that gap.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Ssh;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
// CreateSshConnection() returns ITikCliConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikCliConnection connection = setup.CreateSshConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
// ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
// terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
// the value in 'where' is quoted so a marker with a space or a '/' still parses.
var found = connection.CallCommandSync(
":put [/interface print detail as-value where comment=\"" + Marker + "\"]");
// as-value output comes back as one !re sentence per record, closed by a !done —
// so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" field, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// The CLI addresses the row by that same .id, written as a bare selector.
// A write that succeeds prints nothing at all; a refusal arrives as an exception.
try
{
connection.CallCommandSync(
"/interface set " + id + " comment=\"" + NewComment + "\"");
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: Ssh.
MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server set allowed-interface-list=all # or a list holding your segment
// /user/group set <group> policy=telnet,read,write,... # MAC-Telnet uses 'telnet' (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// No IP route to the router is needed — this is the recovery/bootstrap transport, and a
// router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
// with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
// answers "the MAC of the router at this address", which there is no address to ask about),
// and the local interface is found by trying each adapter until one is answered.
// Host, when passed, still selects the local network path, so keep it pointing at the
// router's segment; RouterMac is what addresses the router.
// If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
// still answer, which makes the failure look like the router's fault. See the linked page.
// Noticeably slower than the IP transports.
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateMacTelnetConnection()
// returns ITikMacCliConnection, which has it.
// The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
// "=.id=...") are NOT translated here. Several rows are joined with one space, so the
// line may be written in one piece or split.
// A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
// script context, so a bare 'print as-value' at a terminal comes back as an empty line.
// 'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
// row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
// mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
// 'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
// A refusal is classified from the output TEXT, because raw mode does not know which verb
// was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
// a way the classifier does not recognise can pass as success. The two levels above are
// verb-aware and do not have that gap.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.MacTelnet;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
RouterMac = RouterMacAddress,
};
// CreateMacTelnetConnection() returns ITikMacCliConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikMacCliConnection connection = setup.CreateMacTelnetConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
// ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
// terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
// the value in 'where' is quoted so a marker with a space or a '/' still parses.
var found = connection.CallCommandSync(
":put [/interface print detail as-value where comment=\"" + Marker + "\"]");
// as-value output comes back as one !re sentence per record, closed by a !done —
// so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" field, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// The CLI addresses the row by that same .id, written as a bare selector.
// A write that succeeds prints nothing at all; a refusal arrives as an exception.
try
{
connection.CallCommandSync(
"/interface set " + id + " comment=\"" + NewComment + "\"");
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: MacTelnet.
WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=winbox] # TCP 8291
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
// Telnet when you cannot use TLS.
// A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateWinboxCliConnection()
// returns ITikCliConnection, which has it.
// The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
// "=.id=...") are NOT translated here. Several rows are joined with one space, so the
// line may be written in one piece or split.
// A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
// script context, so a bare 'print as-value' at a terminal comes back as an empty line.
// 'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
// row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
// mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
// 'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
// A refusal is classified from the output TEXT, because raw mode does not know which verb
// was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
// a way the classifier does not recognise can pass as success. The two levels above are
// verb-aware and do not have that gap.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.WinboxCli;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
// CreateWinboxCliConnection() returns ITikCliConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikCliConnection connection = setup.CreateWinboxCliConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
// ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
// terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
// the value in 'where' is quoted so a marker with a space or a '/' still parses.
var found = connection.CallCommandSync(
":put [/interface print detail as-value where comment=\"" + Marker + "\"]");
// as-value output comes back as one !re sentence per record, closed by a !done —
// so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" field, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// The CLI addresses the row by that same .id, written as a bare selector.
// A write that succeeds prints nothing at all; a refusal arrives as an exception.
try
{
connection.CallCommandSync(
"/interface set " + id + " comment=\"" + NewComment + "\"");
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: WinboxCli.
WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL Low-level API
// TRANSPORT WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server/mac-winbox set allowed-interface-list=all
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// — nothing carries the marker: no interface carries comment=managed-by-tik4net
// — the row went away in between: update refused: no such item (4)
//
// WATCH OUT
// Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
// both properties; otherwise WinboxCli or MacTelnet is faster.
// CallCommandSync is on ITikRawSentenceConnection, not on ITikConnection — CreateWinboxCliMacConnection()
// returns ITikMacCliConnection, which has it.
// The rows are a CLI LINE and are sent unchanged: API sentence words ("?comment=...",
// "=.id=...") are NOT translated here. Several rows are joined with one space, so the
// line may be written in one piece or split.
// A read must be wrapped in ':put [ ... ]' — RouterOS materialises as-value output only in
// script context, so a bare 'print as-value' at a terminal comes back as an empty line.
// 'print as-value' returns the CLI's SUMMARY columns: 8 fields for a commented /interface
// row, where the binary API returns 28. 'detail' raises that to 14 (it adds default-name,
// mtu, vrf, the two link times and link-downs) — the byte counters are in NEITHER and need
// 'print stats'. The O/R mapper level asks for all of this itself and is unaffected.
// A refusal is classified from the output TEXT, because raw mode does not know which verb
// was sent: "no such item (4)" arrives as TikNoSuchItemException, but a refusal worded in
// a way the classifier does not recognise can pass as success. The two levels above are
// verb-aware and do not have that gap.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.WinboxCliMac;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC; omit for MNDP discovery (up to 5 s).
RouterMac = RouterMacAddress,
};
// CreateWinboxCliMacConnection() returns ITikMacCliConnection, which carries CallCommandSync.
// A variable declared ITikConnection would not compile — see WATCH OUT.
using (ITikMacCliConnection connection = setup.CreateWinboxCliMacConnection())
{
// ── FIND ────────────────────────────────────────────────────────────────────
// A RouterOS CLI LINE, sent verbatim — no API sentence words here. The
// ':put [ ... ]' wrapper is not decoration: a bare 'print as-value' typed at a
// terminal prints NOTHING. 'detail' widens the column set (see WATCH OUT), and
// the value in 'where' is quoted so a marker with a space or a '/' still parses.
var found = connection.CallCommandSync(
":put [/interface print detail as-value where comment=\"" + Marker + "\"]");
// as-value output comes back as one !re sentence per record, closed by a !done —
// so select by type, never by position.
var row = found.OfType<ITikReSentence>().SingleOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId(); // the ".id" field, router format "*2"
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// A field the router did not send is ABSENT, not empty — GetResponseField
// throws for it, so read optional fields with GetResponseFieldOrDefault.
string before = row.GetResponseFieldOrDefault("comment", "<unset>");
// ── UPDATE ──────────────────────────────────────────────────────────────────
// The CLI addresses the row by that same .id, written as a bare selector.
// A write that succeeds prints nothing at all; a refusal arrives as an exception.
try
{
connection.CallCommandSync(
"/interface set " + id + " comment=\"" + NewComment + "\"");
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + before + ")");
}
}
}Transport reference: WinboxCliMac.
WinboxNative — structured WinBox M2, TCP 8291 · this level does not exist here
There is no low-level program for native WinBox, because there is no text to send. This transport
speaks the same structured M2 messages the WinBox GUI does: a numeric handler, a command number and fields
addressed by numeric key, resolved from the router's own catalog. That is a request shape, not a command
language — nothing a caller could type and have passed through — so WinboxNativeConnection
implements neither ITikRawSentenceConnection nor
RawCommand:
using tik4net;
using tik4net.WinboxNative;
var setup = new TikConnectionSetup(Host, User, Password);
using ITikWinboxNativeConnection connection = setup.CreateWinboxNativeConnection();
connection.Supports(TikConnectionCapability.RawCommand); // false
// connection.CallCommandSync(...); // does not compile — no such method
connection.CreateRawCommand("/interface print"); // throws TikConnectionCapabilityNotSupportedExceptionUse one of the two levels above instead: the ADO.NET-like WinboxNative tab and the
O/R mapper WinboxNative tab do this task over the same connection, with the path
and fields resolved for you. A path WinBox has no window for is refused with TikPathNotMappedException
rather than answered with wrong data.
Transport reference: WinboxNative.
WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561 · this level does not exist here
Same as WinboxNative: structured M2, so nothing to write raw. The MAC layer only changes how the M2
message is carried; the message is still numeric handlers and field keys, and no
RawCommand is declared.
using tik4net;
using tik4net.WinboxNativeMac;
var setup = new TikConnectionSetup(Host, User, Password) { RouterMac = "AA:BB:CC:DD:EE:FF" };
using ITikWinboxNativeMacConnection connection = setup.CreateWinboxNativeMacConnection();
connection.Supports(TikConnectionCapability.RawCommand); // falseUse one of the two levels above instead: the ADO.NET-like WinboxNativeMac tab and
the O/R mapper WinboxNativeMac tab. Note there what is said about the idle
session: an M2 session over the MAC layer is dropped by the router after roughly 60 s idle, a read is
reopened and reissued automatically, a write never is.
Transport reference: WinboxNativeMac.
commands, parameters, typed Execute* calls
One complete, copy-and-run program per transport — pick your tab:
Api — binary API, TCP 8728
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=api] # TCP 8728
// /user/group set <group> policy=api,read,write,... # the 'api' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// A refused command arrives as a !trap SENTENCE on this transport and does NOT throw at
// the low-level API. This is the only family that behaves that way — see the trap note
// in the sample below.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Api))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: Api.
ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /certificate add name=server common-name=<router> ; /certificate sign server
// /ip/service set api-ssl certificate=server disabled=no # TCP 8729
// /user/group set <group> policy=api,read,write,... # same 'api' policy as Api
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Use CertificateValidationCallback instead of AllowInvalidCertificate in production —
// the flag disables validation entirely.
// A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
// default since 4.0, and without this the open fails with TikConnectionSSLErrorException
// ("The remote certificate was rejected...") before authentication is even attempted.
AllowInvalidCertificate = true,
};
using (ITikConnection connection = setup.Create(TikConnectionType.ApiSsl))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: ApiSsl.
Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=www] # TCP 80
// /user/group set <group> policy=rest-api,read,write,... # 'rest-api', NOT 'api'
// # RouterOS 7.1 or newer — the REST endpoint does not exist on 6.x
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// A missing 'rest-api' policy surfaces as HTTP 401 Unauthorized, which reads exactly like
// a wrong password (measured).
// REST buffers the whole response before returning it, so it is not a streaming transport
// and it has no Safe Mode.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Rest))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: Rest.
RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /certificate add name=server common-name=<router> ; /certificate sign server
// /ip/service set www-ssl certificate=server disabled=no # TCP 443
// /user/group set <group> policy=rest-api,read,write,...
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// If 'reverse-proxy' also listens on 443, only one of the two can own the port.
// Same buffering and Safe Mode limits as Rest.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// Self-signed lab certificate — see the ApiSsl tab; false by default since 4.0.
AllowInvalidCertificate = true,
};
using (ITikConnection connection = setup.Create(TikConnectionType.RestSsl))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: RestSsl.
Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=telnet] # TCP 23
// /user/group set <group> policy=telnet,read,write,... # the 'telnet' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
// network you do not control.
// A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
// or password" — it looks like a wrong password but is not (measured).
// On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
// columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
// 'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
// link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
// level asks for all of this itself and is unaffected.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Telnet))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Ask for the full column set (see WATCH OUT). 'detail' is a name-value
// word, not a filter, so its format is given explicitly.
findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: Telnet.
Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=ssh] # TCP 22
// /user/group set <group> policy=ssh,read,write,... # the 'ssh' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// The ONLY transport that needs a registration call, because it ships in its own NuGet
// package (tik4net.ssh, which pulls in Renci.SshNet). Without Register() the Create call
// throws NotImplementedException.
// A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
// On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
// columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
// 'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
// link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
// level asks for all of this itself and is unaffected.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Ssh;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
Tik4NetSsh.Register(); // once at startup — see the note above the sample
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Ssh))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Ask for the full column set (see WATCH OUT). 'detail' is a name-value
// word, not a filter, so its format is given explicitly.
findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: Ssh.
MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server set allowed-interface-list=all # or a list holding your segment
// /user/group set <group> policy=telnet,read,write,... # MAC-Telnet uses 'telnet' (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// No IP route to the router is needed — this is the recovery/bootstrap transport, and a
// router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
// with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
// answers "the MAC of the router at this address", which there is no address to ask about),
// and the local interface is found by trying each adapter until one is answered.
// Host, when passed, still selects the local network path, so keep it pointing at the
// router's segment; RouterMac is what addresses the router.
// If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
// still answer, which makes the failure look like the router's fault. See the linked page.
// Noticeably slower than the IP transports.
// On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
// columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
// 'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
// link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
// level asks for all of this itself and is unaffected.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.MacTelnet))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Ask for the full column set (see WATCH OUT). 'detail' is a name-value
// word, not a filter, so its format is given explicitly.
findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: MacTelnet.
WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=winbox] # TCP 8291
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
// Telnet when you cannot use TLS.
// A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
// On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
// columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
// 'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
// link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
// level asks for all of this itself and is unaffected.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCli))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Ask for the full column set (see WATCH OUT). 'detail' is a name-value
// word, not a filter, so its format is given explicitly.
findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: WinboxCli.
WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server/mac-winbox set allowed-interface-list=all
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
// both properties; otherwise WinboxCli or MacTelnet is faster.
// On this transport a bare 'print' becomes 'print as-value', which returns the CLI's SUMMARY
// columns only — 8 fields for /interface, where the binary API returns 28. The sample adds
// 'detail', which raises that to 14 (default-name, mtu, vrf, the two link times and
// link-downs) — the byte counters are in NEITHER and need 'print stats'. The O/R mapper
// level asks for all of this itself and is unaffected.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC; omit for MNDP discovery (up to 5 s).
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCliMac))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Ask for the full column set (see WATCH OUT). 'detail' is a name-value
// word, not a filter, so its format is given explicitly.
findCmd.AddParameter("detail", "", TikCommandParameterFormat.NameValue);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: WinboxCliMac.
WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=winbox] # TCP 8291
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Not a CLI: the call becomes the same structured M2 request the WinBox GUI makes, with
// fields addressed by numeric key.
// A path or field WinBox has no window for cannot be reached — you get
// TikPathNotMappedException rather than wrong data. The same path usually works over
// Api or a CLI transport.
// Rows carry a few native-only fields the API does not report.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNative))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: WinboxNative.
WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL ADO.NET-like API
// TRANSPORT WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server/mac-winbox set allowed-interface-list=all
// /user/group set <group> policy=winbox,read,write,...
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Everything in the WinboxNative tab applies, over the MAC carrier.
// An M2 session over the MAC layer is dropped by the router after roughly 60 s idle; a
// read is reopened and reissued automatically, a write never is.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC; omit for MNDP discovery (up to 5 s).
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNativeMac))
{
// ── FIND ────────────────────────────────────────────────────────────
// The parameter FORMAT is not stated here — the Execute* call decides it.
// ExecuteSingleRowOrDefault reads, so 'comment' goes out as ?comment=… .
ITikCommand findCmd = connection.CreateCommandAndParameters(
"/interface/print", "comment", Marker);
// Returns null for "no such row" and THROWS if the filter matched more than
// one — which is what you want for a marker comment that must be unique.
ITikReSentence row = findCmd.ExecuteSingleRowOrDefault();
if (row == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
string id = row.GetId();
Console.WriteLine("found .id=" + id + " name=" + row.GetResponseField("name"));
// ── UPDATE ──────────────────────────────────────────────────────────
// ExecuteNonQuery WRITES, so the same helper now emits =comment=… and
// =.id=… — which is why the id can be passed as an ordinary parameter.
ITikCommand updateCmd = connection.CreateCommandAndParameters(
"/interface/set",
"comment", NewComment,
TikSpecialProperties.Id, id);
// Identical error handling on all eleven transports: a refusal throws.
try
{
updateCmd.ExecuteNonQuery();
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment);
}
}
}Transport reference: WinboxNativeMac.
typed entities, the object carries the .id
One complete, copy-and-run program per transport — pick your tab:
Api — binary API, TCP 8728
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT Api — binary API, TCP 8728
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=api] # TCP 8728
// /user/group set <group> policy=api,read,write,... # the 'api' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// A refused command arrives as a !trap SENTENCE on this transport and does NOT throw at
// the low-level API. This is the only family that behaves that way — see the trap note
// in the sample below.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Api))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: Api.
ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT ApiSsl — binary API over TLS, TCP 8729
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /certificate add name=server common-name=<router> ; /certificate sign server
// /ip/service set api-ssl certificate=server disabled=no # TCP 8729
// /user/group set <group> policy=api,read,write,... # same 'api' policy as Api
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Use CertificateValidationCallback instead of AllowInvalidCertificate in production —
// the flag disables validation entirely.
// A refused command arrives as a !trap SENTENCE here too (binary API family).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The lab router's certificate is self-signed. AllowInvalidCertificate is FALSE by
// default since 4.0, and without this the open fails with TikConnectionSSLErrorException
// ("The remote certificate was rejected...") before authentication is even attempted.
AllowInvalidCertificate = true,
};
using (ITikConnection connection = setup.Create(TikConnectionType.ApiSsl))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: ApiSsl.
Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT Rest — HTTP REST API, TCP 80 (RouterOS 7.1+)
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=www] # TCP 80
// /user/group set <group> policy=rest-api,read,write,... # 'rest-api', NOT 'api'
// # RouterOS 7.1 or newer — the REST endpoint does not exist on 6.x
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// A missing 'rest-api' policy surfaces as HTTP 401 Unauthorized, which reads exactly like
// a wrong password (measured).
// REST buffers the whole response before returning it, so it is not a streaming transport
// and it has no Safe Mode.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Rest))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: Rest.
RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT RestSsl — HTTP REST API over TLS, TCP 443
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /certificate add name=server common-name=<router> ; /certificate sign server
// /ip/service set www-ssl certificate=server disabled=no # TCP 443
// /user/group set <group> policy=rest-api,read,write,...
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// If 'reverse-proxy' also listens on 443, only one of the two can own the port.
// Same buffering and Safe Mode limits as Rest.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// Self-signed lab certificate — see the ApiSsl tab; false by default since 4.0.
AllowInvalidCertificate = true,
};
using (ITikConnection connection = setup.Create(TikConnectionType.RestSsl))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: RestSsl.
Telnet — plain-text CLI, TCP 23
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT Telnet — plain-text CLI, TCP 23
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=telnet] # TCP 23
// /user/group set <group> policy=telnet,read,write,... # the 'telnet' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Credentials and every command travel in CLEAR TEXT. Prefer Ssh or WinboxCli on any
// network you do not control.
// A missing 'telnet' policy is reported by the router as "Login failed, incorrect username
// or password" — it looks like a wrong password but is not (measured).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Telnet))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: Telnet.
Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT Ssh — CLI over an SSH shell, TCP 22
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=ssh] # TCP 22
// /user/group set <group> policy=ssh,read,write,... # the 'ssh' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// The ONLY transport that needs a registration call, because it ships in its own NuGet
// package (tik4net.ssh, which pulls in Renci.SshNet). Without Register() the Create call
// throws NotImplementedException.
// A missing 'ssh' policy surfaces as "Permission denied (password)" (measured).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
using tik4net.Ssh;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
Tik4NetSsh.Register(); // once at startup — see the note above the sample
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.Ssh))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: Ssh.
MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT MacTelnet — CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server set allowed-interface-list=all # or a list holding your segment
// /user/group set <group> policy=telnet,read,write,... # MAC-Telnet uses 'telnet' (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// No IP route to the router is needed — this is the recovery/bootstrap transport, and a
// router with NO IP ADDRESS AT ALL is reachable: drop Host entirely and address the setup
// with TikRouterAddress.FromMac(RouterMacAddress). The MAC then has to be given (MNDP
// answers "the MAC of the router at this address", which there is no address to ask about),
// and the local interface is found by trying each adapter until one is answered.
// Host, when passed, still selects the local network path, so keep it pointing at the
// router's segment; RouterMac is what addresses the router.
// If your machine has several NICs, broadcast can leave through the wrong one — MNDP will
// still answer, which makes the failure look like the router's fault. See the linked page.
// Noticeably slower than the IP transports.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC. Leave this out (or null) and MNDP discovers it — up to 5 s.
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.MacTelnet))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: MacTelnet.
WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT WinboxCli — encrypted CLI over the WinBox channel, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=winbox] # TCP 8291
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Encrypted (EC-SRP5 + AES) with no certificates to manage — the practical alternative to
// Telnet when you cannot use TLS.
// A missing 'winbox' policy is refused with "std failure: not allowed (9)" (measured).
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCli))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: WinboxCli.
WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT WinboxCliMac — encrypted WinBox CLI over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server/mac-winbox set allowed-interface-list=all
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy (measured)
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Encrypted AND routeless at once — and the slowest of the eleven. Use it when you need
// both properties; otherwise WinboxCli or MacTelnet is faster.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC; omit for MNDP discovery (up to 5 s).
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxCliMac))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: WinboxCliMac.
WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT WinboxNative — structured WinBox M2, TCP 8291
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /ip/service enable [find name=winbox] # TCP 8291
// /user/group set <group> policy=winbox,read,write,... # the 'winbox' policy is required
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Not a CLI: the call becomes the same structured M2 request the WinBox GUI makes, with
// fields addressed by numeric key.
// A path or field WinBox has no window for cannot be reached — you get
// TikPathNotMappedException rather than wrong data. The same path usually works over
// Api or a CLI transport.
// Rows carry a few native-only fields the API does not report.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password);
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNative))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: WinboxNative.
WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// TASK find the interface commented "managed-by-tik4net", keep its .id,
// then write a new comment back addressing the row by that .id
// LEVEL High-level O/R mapper
// TRANSPORT WinboxNativeMac — structured WinBox M2 over the MAC layer, UDP 20561
// ==========================================================================
// ON THE ROUTER — must be prepared before this runs:
// /tool/mac-server/mac-winbox set allowed-interface-list=all
// /user/group set <group> policy=winbox,read,write,...
// # your machine must be in the router's LAYER-2 broadcast domain
// /interface set [find default-name=ether1] comment="managed-by-tik4net"
//
// INPUT Host / User / Password — router coordinates
// RouterMacAddress — the router's MAC; omit it to discover by MNDP (needs Host)
// Marker — the comment that identifies the row
// NewComment — what gets written back
//
// OUTPUT prints found .id=*2 name=ether1 mtu=1500
// updated, comment is now: managed-by-tik4net (checked)
// prints "no interface carries comment=..." and returns when nothing matches
//
// WATCH OUT
// Everything in the WinboxNative tab applies, over the MAC carrier.
// An M2 session over the MAC layer is dropped by the router after roughly 60 s idle; a
// read is reopened and reissued automatically, a write never is.
// ==========================================================================
using System;
using System.Linq;
using tik4net;
using tik4net.Objects;
using tik4net.Objects.Interface;
class Program
{
const string Host = "192.168.88.1"; // the router's address
const string User = "admin";
const string Password = "";
const string RouterMacAddress = "AA:BB:CC:DD:EE:FF"; // omit to discover the router by MNDP
const string Marker = "managed-by-tik4net"; // the comment that identifies the row
const string NewComment = "managed-by-tik4net (checked)";
static void Main()
{
var setup = new TikConnectionSetup(Host, User, Password)
{
// The router's MAC; omit for MNDP discovery (up to 5 s).
RouterMac = RouterMacAddress,
};
using (ITikConnection connection = setup.Create(TikConnectionType.WinboxNativeMac))
{
// ── FIND ────────────────────────────────────────────────────────────
// The entity's own metadata decides what is asked for, so the read is
// complete on every transport — including the CLI ones, where a hand-written
// print would return the summary columns only.
Interface iface = connection.LoadSingleOrDefault<Interface>(
connection.CreateParameter("comment", Marker));
// null = nothing matched. LoadSingle<T> is the same query that throws instead;
// either way, more than one match throws.
if (iface == null)
{
Console.WriteLine("no interface carries comment=" + Marker);
return;
}
// Properties are nullable since 4.0: a field the router did not send is null,
// not "" or 0.
Console.WriteLine("found .id=" + iface.Id + " name=" + iface.Name
+ " mtu=" + iface.Mtu);
// ── UPDATE ──────────────────────────────────────────────────────────
// No .id is spelled out: the entity carries it and Save() addresses the row
// with it. Change tracking diffs against the load-time snapshot, so what goes
// on the wire is /interface/set =.id=… =comment=… — one field, not the row.
// A Save with nothing changed makes no call at all.
iface.Comment = NewComment;
try
{
connection.Save(iface);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + iface.Comment);
}
}
}Transport reference: WinboxNativeMac.
Measured on RouterOS 7.24 by running every program on this page against a live router — 29 of 29 found
the row and wrote the comment through. (29, not 33: the low level does not exist on Rest, RestSsl,
WinboxNative and WinboxNativeMac, so those four tabs are an explanation rather than a program.) So the
table is about what the reply looks like, not about whether the task works somewhere and not elsewhere:
Api · ApiSsl
|
Rest · RestSsl
|
CLI family ( Telnet · Ssh · MacTelnet · WinboxCli · WinboxCliMac) |
WinboxNative · WinboxNativeMac
|
|
|---|---|---|---|---|
| Filter by comment | ✔ | ✔ | ✔ (where comment=…) |
✔ (filtered client-side) |
.id returned |
*2 |
*2 |
*2 |
*2 |
Update by .id
|
✔ | ✔ | ✔ | ✔ |
Fields on a bare print
|
all | all | summary columns only | all, plus native-only extras |
| The low-level API at all | API sentence words | not available | RouterOS CLI text | not available |
| Refusal at the low-level API | ITikTrapSentence |
— | throws | — |
| Refusal at the other two levels | throws | throws | throws | throws |
Three rows deserve more than a tick.
The low level exists only where the transport has a command language. Api/ApiSsl have sentence
words and the CLI family has the RouterOS command line; REST has an HTTP request shape and native WinBox a
numeric M2 message, neither of which a caller can write out, so they declare no RawCommand and
CallCommandSync is not on their connection types at all. The two levels above are portable across all
eleven.
The trap sentence exists on Api/ApiSsl only. They speak the binary protocol, where a refusal really
is a !trap sentence on the wire, so the low-level API hands it back and the call does not throw. Every
other transport emulates the sentence protocol on top of a CLI/REST/M2 exchange, has no !trap to hand
back, and throws instead. Code written against sentence inspection therefore silently stops checking
anything when you move it to another transport — which is why the low-level tabs differ between the two
families, and why the ADO.NET-like level and above are the portable way to write error handling.
A bare print on a CLI transport returns the summary columns. It becomes print as-value, which for a
commented /interface row is 8 fields against the binary API's 28 — mtu, default-name, vrf and the
link times are simply absent. Adding detail brings those back (14 fields), but not the byte counters:
those come from print stats, which is a separate query. The CLI tabs at the low level add detail
themselves. The O/R mapper is unaffected: an entity declares IncludeDetails / IncludeCliStats and
the mapper adds both markers to every load, so a typed Interface is complete on all eleven transports.
At the ADO.NET-like level and above: everything except the Create(...) line and the transport's own
prerequisites. Within one of those two levels the body is the same code on all eleven transports. That is
the point of ITikConnection: pick the transport at the edge of your program, and the rest of it does not
know which one it got.
The low level is the exception, by design — it is where you go because you want the transport's own language — so its programs differ between the API family and the CLI family, and four transports have no such level at all.
Options are applied through TikConnectionSetup rather than set on the concrete connection type — a
transport declares what it can honour, so RouterMac on a TCP transport or AllowInvalidCertificate on a
plaintext one is simply not applied rather than silently half-applied.
How each call is translated per transport is written up in Command translation on non-API transports; which transport can do what at all is the matrix in Connection types & capabilities.
- CRUD examples for all APIs — create/read/update/delete per API level
-
Connection types & capabilities — the capability matrix and
TikConnectionSetup -
Command translation on non-API transports — what each
Execute*becomes on the wire -
Change tracking — why
Savesends one field -
Exception handling — the exception hierarchy behind
TikCommandTrapException -
tik4net.examples/OneTaskEveryTransportExamples.cs— the same three levels as compiled code