MikrotikApi

An Elixir wrapper for MikroTik RouterOS REST API. Auth is established once and passed per call alongside a simple target IP (IPv4/IPv6). We bias toward programmatic usage with POST for create/command-style operations while supporting standard REST verbs.

Reference: MikroTik RouterOS REST API — https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API

Goals

See rest_api.md for the full specification and plan.

Installation

Add to your deps from Hex:

# mix.exs (in your host application)
def deps do
[
{:mikrotik_api, "~> 0.3"}
]
end

Quick Start

Transport configuration

Examples:

# config/runtime.exs (in your host application)
import Config
config :mikrotik_api, default_scheme: :http
# For HTTPS by default instead:
# config :mikrotik_api, default_scheme: :https
# Establish auth once; target is just an IP
auth = MikrotikApi.Auth.new(
username: System.get_env("MT_USER"),
password: System.get_env("MT_PASS"),
verify: :verify_peer
)
ip = "10.0.0.1"
# GET system resource over WireGuard (HTTP inside private network)
# If you want HTTPS, set default_scheme: :https or pass scheme: :https per call.
case MikrotikApi.get(auth, ip, "/system/resource", scheme: :http) do
{:ok, data} -> Logger.info("system resource ok")
{:error, err} -> Logger.error("system resource failed: #{inspect(err)}")
end
# POST to create an IP address (programmatic workflow)
attrs = %{"address" => "192.168.88.2/24", "interface" => "bridge"}
# For HTTPS with self-signed certs in lab, you can use verify: :verify_none (accepting the risk):
# auth = MikrotikApi.Auth.new(username: ..., password: ..., verify: :verify_none)
# For HTTPS with real certs, prefer verify: :verify_peer and provide CA info if needed:
# auth = MikrotikApi.Auth.new(username: ..., password: ..., verify: :verify_peer, ssl_opts: [cacertfile: '/etc/ssl/certs/ca-bundle.crt'])
case MikrotikApi.post(auth, ip, "/ip/address", attrs, scheme: :http) do
{:ok, created} -> Logger.info("added ip address")
{:error, err} -> Logger.error("add ip failed: #{inspect(err)}")
end

Testing With Mocked Routers

MikrotikApi.Transport.Mock is included for tests that need deterministic RouterOS endpoint responses without a physical device.

# test/test_helper.exs or per-test setup
Application.put_env(:mikrotik_api, :transport, MikrotikApi.Transport.Mock)

Stub RouterOS REST paths without the /rest prefix:

defmodule MyRouterTest do
use ExUnit.Case, async: true
alias MikrotikApi.Auth
alias MikrotikApi.Transport.Mock
setup do
Application.put_env(:mikrotik_api, :transport, Mock)
Mock.clear()
:ok
end
test "reads system resources" do
Mock.stub(:get, "/system/resource", 200, %{
"uptime" => "1h",
"board-name" => "hAP ax2"
})
auth = Auth.new(username: "u", password: "p")
assert {:ok, %{"uptime" => "1h"}} =
MikrotikApi.system_resource(auth, "10.0.0.1", scheme: :http)
end
end

For tests that need to assert on headers, request bodies, TLS options, or the fully built URL, install a low-level handler:

Mock.put(fn method, url, headers, body, opts ->
assert method == :put
assert to_string(url) == "http://10.0.0.1:80/rest/ip/address"
assert Enum.any?(headers, fn {key, _} -> to_string(key) == "authorization" end)
assert Keyword.has_key?(opts, :http_opts)
{:ok, {201, [], ""}}
end)

Mocks are isolated by the calling process. If the code under test calls MikrotikApi from a worker process, pass owner_pid: self() to the API call so the worker can use the test process' stubs.

Security Notes

API Overview

Telemetry helpers (Phase 1–4)

Core functions (generic verbs)

Common opts

Helper functions (selected)

HTTP over WireGuard (decode: true)

ARP and neighbors

auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ip = System.get_env("MT_IP")
{:ok, arp} = MikrotikApi.arp_list(auth, ip, scheme: :http)
{:ok, neighbors} = MikrotikApi.neighbor_list(auth, ip, scheme: :http)
auth = MikrotikApi.Auth.new(
username: System.get_env("MT_USER"),
password: System.get_env("MT_PASS"),
verify: :verify_none
)
ip = System.get_env("MT_IP")
{:ok, sys} = MikrotikApi.system_resource(auth, ip, scheme: :http)
{:ok, ip_addrs} = MikrotikApi.ip_address_list(auth, ip, scheme: :http)

WiFi notes

CAPsMAN examples

auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ip = System.get_env("MT_IP")
{:ok, caps_if} = MikrotikApi.capsman_interface_list(auth, ip, scheme: :http)
{:ok, caps_sec} = MikrotikApi.capsman_security_list(auth, ip, scheme: :http)
{:ok, caps_reg} = MikrotikApi.capsman_registration_table(auth, ip, scheme: :http)

Probe examples

auth = MikrotikApi.Auth.new(
username: System.get_env("MT_USER"),
password: System.get_env("MT_PASS"),
verify: :verify_none
)
ip = System.get_env("MT_IP")
{:ok, summary} = MikrotikApi.probe_wireless(auth, ip, scheme: :http)
# summary => %{wireless: %{...}, wifi: %{...}}
auth = MikrotikApi.Auth.new(
username: System.get_env("MT_USER"),
password: System.get_env("MT_PASS"),
verify: :verify_none
)
ip = System.get_env("MT_IP")
{:ok, summary} = MikrotikApi.probe_device(auth, ip, scheme: :http)
# summary => %{system: {:ok, %{...}}, counts: %{interfaces: n, ip_addresses: n, arp: n, neighbors: n}}

Batch reads (multi)

To fetch the same path across multiple devices concurrently, use multi/6.

Example:

auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ips = ["192.168.88.1", "192.168.88.2"]
results = MikrotikApi.multi(auth, ips, :get, "/system/resource", [scheme: :http], max_concurrency: 5, timeout: 10_000)
# [%{ip: "192.168.88.1", result: {:ok, %{...}}}, ...]

Developer guardrails

To prevent regressions, run:

mix guardrails

This task scans for disallowed patterns (IO.puts/IO.inspect and the legacy MikrotikApi.JSON).

Normalization helpers (optional)

The library includes optional utilities for exporters to normalize string fields commonly found in RouterOS responses.

Examples:

# Normalize wireless registration-table entries (legacy wireless)
{:ok, regs} = MikrotikApi.wireless_registration_table(auth, ip, scheme: :http)
normalized =
Enum.map(regs, fn e ->
e
|> Map.update("rx-signal", nil, &MikrotikApi.Normalize.to_int/1)
|> Map.update("tx-rate", nil, &MikrotikApi.Normalize.parse_rate_mbps/1)
|> Map.update("rx-rate", nil, &MikrotikApi.Normalize.parse_rate_mbps/1)
end)
# Normalize booleans
val = MikrotikApi.Normalize.normalize_bool("enabled") # => true

Multi (concurrent) examples

auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ips = ["192.168.88.1", "192.168.88.2", "192.168.88.3"]
results = MikrotikApi.multi(auth, ips, :get, "/system/resource", [scheme: :http], max_concurrency: 5, timeout: 10_000)
# results => [%{ip: "192.168.88.1", result: {:ok, %{...}}}, ...]

Ensure examples

# Route ensure
attrs = %{"dst-address" => "10.10.0.0/16", "gateway" => "192.168.88.1"}
{:ok, %{dst: _, gw: _}} = MikrotikApi.route_ensure(auth, ip, attrs, scheme: :http)
# Bridge and ports/VLANs ensure
{:ok, "bridgeLocal"} = MikrotikApi.bridge_ensure(auth, ip, "bridgeLocal", %{}, scheme: :http)
{:ok, {"bridgeLocal", "ether2"}} = MikrotikApi.bridge_port_ensure(auth, ip, "bridgeLocal", "ether2", %{}, scheme: :http)
{:ok, {"bridgeLocal", "10"}} = MikrotikApi.bridge_vlan_ensure(auth, ip, "bridgeLocal", "10", %{"tagged" => "sfp-sfpplus1", "untagged" => "ether2"}, scheme: :http)
# Interface ensure (patch only changed keys)
{:ok, %{changed: _}} = MikrotikApi.interface_ensure(auth, ip, "ether1", %{"mtu" => "1500", "disabled" => "false"}, scheme: :http)
# IP address ensure
{:ok, _addr} = MikrotikApi.ip_address_ensure(auth, ip, %{"address" => "192.168.88.2/24", "interface" => "bridgeLocal"}, scheme: :http)
# DHCP lease ensure
{:ok, _lease} = MikrotikApi.dhcp_lease_ensure(auth, ip, %{"address" => "192.168.88.100", "mac-address" => "AA:BB:CC:DD:EE:FF"}, scheme: :http)
# Firewall filter ensure
rule = %{"chain" => "forward", "action" => "accept", "comment" => "allow"}
{:ok, _} = MikrotikApi.firewall_filter_ensure(auth, ip, rule, scheme: :http)
# Firewall NAT ensure (default match by chain+action)
nat_rule = %{"chain" => "dstnat", "action" => "dst-nat", "to-addresses" => "192.168.88.2"}
{:ok, _} = MikrotikApi.firewall_nat_ensure(auth, ip, nat_rule, scheme: :http)

WireGuard ensure and pair workflow

# Ensure a WireGuard interface by name on a single router
{:ok, %{name: "wgA"}} = MikrotikApi.wireguard_interface_ensure(
auth,
ip_a,
"wgA",
%{"listen-port" => "51820"},
scheme: :http
)
# Create a pair in a VRRP cluster: create on A, read private-key, apply same key on B
case MikrotikApi.ensure_wireguard_pair(
auth,
ip_a,
"wgA",
ip_b,
"wgB",
%{"listen-port" => "51820"},
scheme: :http
) do
{:ok, %{a: _res_a, b: _res_b}} -> Logger.info("wireguard pair ensured")
{:error, %MikrotikApi.Error{reason: :wireguard_private_key_unreadable}} ->
Logger.warn("RouterOS REST did not return private-key; provide or generate one as a fallback")
other -> Logger.error("pair setup failed: #{inspect(other)}")
end

WiFi ensure workflow (wifiwave2)

# Ensure a wifi security profile, then ensure an SSID using it
{:ok, _} = MikrotikApi.wifi_security_ensure(auth, ip, "SEC-PSK", %{"wpa2-pre-shared-key" => "supersecret"}, scheme: :http)
case MikrotikApi.wifi_ssid_ensure(auth, ip, "WG-LAB", %{"security" => "SEC-PSK"}, scheme: :http) do
{:ok, _} -> :ok
{:error, %MikrotikApi.Error{reason: :wifi_ssid_unavailable}} -> :ok # device may not expose SSIDs
other -> other
end
# Optionally update a wifi interface to reference a configuration (if used in your setup)
# MikrotikApi.wifi_interface_update(auth, ip, "wifi1", %{"disabled" => "false"}, scheme: :http)

Legacy wireless ensure (wireless package)

{:ok, _} = MikrotikApi.wireless_security_profile_ensure(auth, ip, "LEGACY-SEC", %{"mode" => "dynamic-keys"}, scheme: :http)
{:ok, _} = MikrotikApi.wireless_interface_ensure(auth, ip, "wlan1", %{"disabled" => "false"}, scheme: :http)

CAPsMAN ensure examples

{:ok, _} = MikrotikApi.capsman_security_ensure(auth, ip, "CAPS-SEC", %{"authentication-types" => "wpa2-psk"}, scheme: :http)
{:ok, _} = MikrotikApi.capsman_provisioning_ensure(auth, ip, %{"action" => "create-enabled", "master-configuration" => "MASTER"}, scheme: :http)

HTTPS with verify_peer and CA

auth = MikrotikApi.Auth.new(
username: System.get_env("MT_USER"),
password: System.get_env("MT_PASS"),
verify: :verify_peer,
ssl_opts: [cacertfile: "/path/to/ca.pem"]
)
ip = System.get_env("MT_IP")
{:ok, sys} = MikrotikApi.system_resource(auth, ip, scheme: :https)

Reference