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

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

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