#tokio #async #router #api-bindings

no-std mikrotik-rs

Asynchronous Rust library for interfacing with MikroTik routers

15 releases (7 breaking)

Uses new Rust 2024

0.8.0 May 4, 2026
0.6.1 Mar 19, 2026
0.5.1 Dec 14, 2025
0.4.2 Nov 30, 2025
0.1.0 Mar 28, 2024

#136 in Asynchronous

Download history 9564/week @ 2026-02-23 9586/week @ 2026-03-02 10100/week @ 2026-03-09 10630/week @ 2026-03-16 655/week @ 2026-03-23 412/week @ 2026-03-30 661/week @ 2026-04-06 1912/week @ 2026-04-13 1129/week @ 2026-04-20 2025/week @ 2026-04-27 3052/week @ 2026-05-04 3944/week @ 2026-05-11 4225/week @ 2026-05-18 2740/week @ 2026-05-25 111/week @ 2026-06-01 229/week @ 2026-06-08

7,335 downloads per month
Used in 2 crates

AGPL-3.0-only

150KB
2.5K SLoC

mikrotik-rs

docs.rs Crates.io Crates.io License Crates.io Total Downloads GitHub Repo stars

A Rust client for the MikroTik RouterOS API.

Send commands, stream responses, and manage multiple concurrent operations against MikroTik routers with a type-safe, channel-based API.

Highlights

  • Sans-IO protocol core#![no_std]-compatible, runtime-agnostic protocol implementation
  • Tokio adapter — high-level async client with background actor, per-command channels, and TLS support
  • Embassy adapter — embedded-friendly async client, transport-agnostic over embedded-io-async
  • Typestate command builder — the compiler enforces correct command construction order
  • Compile-time command validation — the command! macro validates RouterOS command paths at compile time
  • Zero-copy protocol parsing — words are parsed lazily from the receive buffer with byte-level dispatch
  • Concurrent command execution — each command is tagged with a UUID v4 for response demultiplexing
  • Automatic lifecycle management — dropping a response receiver cancels the command on the router
  • No unsafe codeunsafe_code = "forbid" is enforced at the workspace level

Installation

cargo add mikrotik-rs

Quick start

use mikrotik_rs::{MikrotikDevice, CommandBuilder, Event};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let device = MikrotikDevice::connect("192.168.88.1:8728", "admin", Some("password")).await?;

    let cmd = CommandBuilder::new()
        .command("/system/resource/print")
        .build();

    let mut rx = device.send_command(cmd).await?;

    while let Some(event) = rx.recv().await {
        match event {
            Event::Reply { response, .. } => println!("{:?}", response.attributes),
            Event::Done { .. } => break,
            Event::Trap { response, .. } => eprintln!("error: {}", response.message),
            other => println!("{other:?}"),
        }
    }

    Ok(())
}

MikrotikDevice is cheaply Clone-able — share it across tasks, spawn concurrent commands, and let each one stream responses independently.

Feature flags

Feature Default Description
tokio yes Enables the Tokio async adapter and MikrotikDevice client
embassy no Enables the Embassy embedded async adapter
tokio-tls no Enables TLS support via tokio-rustls
# Plaintext (default)
mikrotik-rs = "0.7"

# TLS for API-SSL (port 8729) — bring your own crypto provider
mikrotik-rs = { version = "0.7", features = ["tokio-tls"] }
rustls = { version = "0.23", features = ["ring"] }  # or "aws-lc-rs"

# Embassy embedded adapter (no_std)
mikrotik-rs = { version = "0.7", default-features = false, features = ["embassy"] }

# Protocol types only (no runtime)
mikrotik-rs = { version = "0.7", default-features = false }

Usage

Building commands

The CommandBuilder uses a typestate pattern: you must call .command() before .attribute() or .build(). The compiler rejects incorrect ordering.

use mikrotik_rs::CommandBuilder;

let cmd = CommandBuilder::new()
    .command("/interface/ethernet/monitor")
    .attribute("numbers", Some("0,1"))
    .attribute("once", None)
    .build();

The command! macro

For static command paths, the command! macro validates the path at compile time:

use mikrotik_rs::command;

let cmd = command!("/interface/print");

let cmd = command!(
    "/interface/ethernet/monitor",
    numbers = "0,1",
    once
);

// These fail at compile time:
// command!("invalid//command");   // no empty segments
// command!("no-leading-slash");   // must start with '/'

Handling responses

Every call to send_command returns an mpsc::Receiver<Event> scoped to that command:

use mikrotik_rs::Event;

let mut rx = device.send_command(cmd).await?;

while let Some(event) = rx.recv().await {
    match event {
        Event::Reply { response, .. } => {
            println!("attributes: {:?}", response.attributes);
        }
        Event::Done { .. } => {
            println!("command completed");
        }
        Event::Trap { response, .. } => {
            eprintln!("trap: {} (category: {:?})", response.message, response.category);
        }
        Event::Fatal { reason } => {
            eprintln!("fatal: {reason}");
        }
        Event::Empty { .. } => {
            // RouterOS 7.18+: command had no data to return
        }
    }
}

TLS connections

With the tokio-tls feature enabled, use the builder for TLS:

// Accept self-signed certs (typical for MikroTik routers)
let device = MikrotikDevice::builder("192.168.88.1:8729")
    .credentials("admin", Some("password"))
    .tls_insecure()
    .connect()
    .await?;

// Or with a custom rustls ClientConfig
let device = MikrotikDevice::builder("192.168.88.1:8729")
    .credentials("admin", Some("password"))
    .tls_config(my_config, server_name)
    .connect()
    .await?;

Streaming responses

Commands that produce continuous output stream results through the same channel:

let monitor = CommandBuilder::new()
    .command("/interface/monitor-traffic")
    .attribute("interface", Some("ether1"))
    .build();

let mut rx = device.send_command(monitor).await?;

while let Some(event) = rx.recv().await {
    println!("{event:?}");
}
// Dropping `rx` automatically sends /cancel to the router

Queries

Filter results using RouterOS query operations:

use mikrotik_rs::{CommandBuilder, QueryOperator};

let cmd = CommandBuilder::new()
    .command("/interface/print")
    .query_equal("type", "ether")
    .query_equal("running", "true")
    .query_operations([QueryOperator::And].into_iter())
    .build();
Method Wire format Description
query_equal(k, v) ?k=v Property equals value
query_gt(k, v) ?>k=v Property greater than value
query_lt(k, v) ?<k=v Property less than value
query_is_present(k) ?k Property exists
query_not_present(k) ?-k Property does not exist
query_operations(ops) ?#... Boolean stack operations (And, Or, Not, Dot)

Workspace

The library is split into focused crates:

Crate Purpose
mikrotik-proto Sans-IO protocol core (#![no_std]) — wire format, commands, responses, connection state machine
mikrotik-tokio Tokio async adapter — background actor, per-command channels, TLS
mikrotik-embassy Embassy embedded async adapter — transport-agnostic over embedded-io-async
mikrotik-rs Convenience re-exports from all crates
┌─────────────────────────────────────────────────────────────────┐
│                        mikrotik-rs                              │
│                    (re-exports from all)                        │
├──────────────────────────┬──────────────────────────────────────┤
│      mikrotik-tokio      │         mikrotik-embassy             │
│   (Tokio async adapter)  │   (Embassy embedded adapter)         │
├──────────────────────────┴──────────────────────────────────────┤
│                       mikrotik-proto                            │
│           Sans-IO protocol core (#![no_std])                    │
└─────────────────────────────────────────────────────────────────┘

Examples

The repository includes runnable examples in examples/.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.

License

Licensed under the GNU Affero General Public License v3.0.

For commercial licensing options (use without AGPL obligations), contact the project maintainer.

Disclaimer

This project is not affiliated with MikroTik. It is an independent, community-developed library.

Dependencies

~1–13MB
~118K SLoC