-
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 complete, runnable program per transport, in its own tab.
Take the tab you need, paste it into a fresh console project, set Host/User/Password, and run it —
nothing is elided, and each sample'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.
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 sample needs, on top of the per-transport prerequisites in each 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 sample; 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.
raw request words in, raw sentences out
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 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 prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── 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. Every other transport throws instead.
ITikTrapSentence trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
if (trap != null)
{
Console.WriteLine("update refused: " + trap.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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
// RouterMacAddress — the router's MAC; omit it to discover by MNDP
// 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) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── 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. Every other transport throws instead.
ITikTrapSentence trap = reply.OfType<ITikTrapSentence>().FirstOrDefault();
if (trap != null)
{
Console.WriteLine("update refused: " + trap.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 Low-level 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) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 Low-level 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
// 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) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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 ────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
// '=detail=' is a name-value word asking for the full column set (see WATCH OUT).
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker,
"=detail=");
// 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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 ────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
// '=detail=' is a name-value word asking for the full column set (see WATCH OUT).
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker,
"=detail=");
// 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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
// 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) (was: managed-by-tik4net)
// 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.
// Host is still passed and 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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 ────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
// '=detail=' is a name-value word asking for the full column set (see WATCH OUT).
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker,
"=detail=");
// 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 prints found .id=*2 name=ether1
// updated, comment is now: managed-by-tik4net (checked) (was: managed-by-tik4net)
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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 ────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
// '=detail=' is a name-value word asking for the full column set (see WATCH OUT).
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker,
"=detail=");
// 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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
// 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) (was: managed-by-tik4net)
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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 ────────────────────────────────────────────────────────────
// '?' words are FILTERS. The router answers one !re sentence per matching
// row and closes the reply with !done.
// '=detail=' is a name-value word asking for the full column set (see WATCH OUT).
var found = connection.CallCommandSync("/interface/print",
"?comment=" + Marker,
"=detail=");
// 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 Low-level 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) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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 Low-level 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
// 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) (was: managed-by-tik4net)
// 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 ────────────────────────────────────────────────────────────
// '?' 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.
ITikReSentence 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", null);
// ── UPDATE ──────────────────────────────────────────────────────────
// '=' words are NAME-VALUE. Addressing the row by .id is what makes this an
// update of that row rather than a second search.
// This transport has no !trap on the wire to hand back, so a refusal arrives
// as an exception. (On Api/ApiSsl the same call returns a trap SENTENCE.)
try
{
connection.CallCommandSync("/interface/set",
"=.id=" + id,
"=comment=" + NewComment);
}
catch (TikCommandException ex)
{
Console.WriteLine("update refused: " + ex.Message);
return;
}
Console.WriteLine("updated, comment is now: " + NewComment
+ " (was: " + (before ?? "<unset>") + ")");
}
}
}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
// 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
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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
// 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.
// Host is still passed and 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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
// 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. mtu, default-name,
// last-link-up-time and the byte counters are simply absent. The sample adds 'detail' to get
// them; the O/R mapper level does this for you 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
// 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
// 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
// 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
// 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.
// Host is still passed and 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
// 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
// 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 the three samples over all eleven transports — 33 of 33 found the row and wrote the comment through. 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 |
| Refusal at the low-level API | ITikTrapSentence |
throws | throws | throws |
| Refusal at the other two levels | throws | throws | throws | throws |
Two rows deserve more than a tick.
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,
last-link-up-time and the byte counters are simply absent. The CLI tabs add detail for that reason. 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.
Everything except the Create(...) line and the transport's own prerequisites. Within one API level the
three bodies are the same code on all eleven transports, with the two exceptions above. 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.
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